aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorDavid Lattimore <[email protected]>2020-07-22 07:46:29 +0100
committerDavid Lattimore <[email protected]>2020-07-24 12:34:00 +0100
commit757f755c29e041fd319af466d7d0418f54cb090a (patch)
tree30d94206c5009730855a2ceaebdf364963358928 /crates
parent3975952601888d9f77e466c12e8e389748984b33 (diff)
SSR: Match paths based on what they resolve to
Also render template paths appropriately for their context.
Diffstat (limited to 'crates')
-rw-r--r--crates/ra_ide/src/ssr.rs12
-rw-r--r--crates/ra_ssr/src/lib.rs61
-rw-r--r--crates/ra_ssr/src/matching.rs106
-rw-r--r--crates/ra_ssr/src/parsing.rs17
-rw-r--r--crates/ra_ssr/src/replacing.rs40
-rw-r--r--crates/ra_ssr/src/resolving.rs153
-rw-r--r--crates/ra_ssr/src/search.rs8
-rw-r--r--crates/ra_ssr/src/tests.rs142
-rw-r--r--crates/rust-analyzer/src/cli/ssr.rs4
9 files changed, 482 insertions, 61 deletions
diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs
index 3e2705d62..2f40bac08 100644
--- a/crates/ra_ide/src/ssr.rs
+++ b/crates/ra_ide/src/ssr.rs
@@ -11,6 +11,16 @@ use ra_ssr::{MatchFinder, SsrError, SsrRule};
11// 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.
12// Within a macro call, a placeholder will match up until whatever token follows the placeholder. 12// Within a macro call, a placeholder will match up until whatever token follows the placeholder.
13// 13//
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//
14// Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`. 24// Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`.
15// 25//
16// Supported constraints: 26// Supported constraints:
@@ -47,7 +57,7 @@ pub fn parse_search_replace(
47) -> Result<Vec<SourceFileEdit>, SsrError> { 57) -> Result<Vec<SourceFileEdit>, SsrError> {
48 let rule: SsrRule = rule.parse()?; 58 let rule: SsrRule = rule.parse()?;
49 let mut match_finder = MatchFinder::in_context(db, position); 59 let mut match_finder = MatchFinder::in_context(db, position);
50 match_finder.add_rule(rule); 60 match_finder.add_rule(rule)?;
51 if parse_only { 61 if parse_only {
52 return Ok(Vec::new()); 62 return Ok(Vec::new());
53 } 63 }
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
index a0a5c9762..286619f59 100644
--- a/crates/ra_ssr/src/lib.rs
+++ b/crates/ra_ssr/src/lib.rs
@@ -7,6 +7,7 @@ mod matching;
7mod nester; 7mod nester;
8mod parsing; 8mod parsing;
9mod replacing; 9mod replacing;
10mod resolving;
10mod search; 11mod search;
11#[macro_use] 12#[macro_use]
12mod errors; 13mod errors;
@@ -21,6 +22,7 @@ use hir::Semantics;
21use ra_db::{FileId, FilePosition, FileRange}; 22use ra_db::{FileId, FilePosition, FileRange};
22use ra_ide_db::source_change::SourceFileEdit; 23use ra_ide_db::source_change::SourceFileEdit;
23use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; 24use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
25use resolving::ResolvedRule;
24use rustc_hash::FxHashMap; 26use rustc_hash::FxHashMap;
25 27
26// A structured search replace rule. Create by calling `parse` on a str. 28// A structured search replace rule. Create by calling `parse` on a str.
@@ -48,7 +50,9 @@ pub struct SsrMatches {
48pub struct MatchFinder<'db> { 50pub struct MatchFinder<'db> {
49 /// Our source of information about the user's code. 51 /// Our source of information about the user's code.
50 sema: Semantics<'db, ra_ide_db::RootDatabase>, 52 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,
52} 56}
53 57
54impl<'db> MatchFinder<'db> { 58impl<'db> MatchFinder<'db> {
@@ -56,10 +60,24 @@ impl<'db> MatchFinder<'db> {
56 /// `lookup_context`. 60 /// `lookup_context`.
57 pub fn in_context( 61 pub fn in_context(
58 db: &'db ra_ide_db::RootDatabase, 62 db: &'db ra_ide_db::RootDatabase,
59 _lookup_context: FilePosition, 63 lookup_context: FilePosition,
60 ) -> MatchFinder<'db> { 64 ) -> MatchFinder<'db> {
61 // FIXME: Use lookup_context 65 let sema = Semantics::new(db);
62 MatchFinder { sema: Semantics::new(db), rules: Vec::new() } 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 }
63 } 81 }
64 82
65 /// Constructs an instance using the start of the first file in `db` as the lookup context. 83 /// Constructs an instance using the start of the first file in `db` as the lookup context.
@@ -84,8 +102,16 @@ impl<'db> MatchFinder<'db> {
84 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take 102 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
85 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to 103 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
86 /// match to it. 104 /// match to it.
87 pub fn add_rule(&mut self, rule: SsrRule) { 105 pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
88 self.add_parsed_rules(rule.parsed_rules); 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(())
89 } 115 }
90 116
91 /// Finds matches for all added rules and returns edits for all found matches. 117 /// Finds matches for all added rules and returns edits for all found matches.
@@ -110,8 +136,16 @@ impl<'db> MatchFinder<'db> {
110 136
111 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you 137 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
112 /// intend to do replacement, use `add_rule` instead. 138 /// intend to do replacement, use `add_rule` instead.
113 pub fn add_search_pattern(&mut self, pattern: SsrPattern) { 139 pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
114 self.add_parsed_rules(pattern.parsed_rules); 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(())
115 } 149 }
116 150
117 /// Returns matches for all added rules. 151 /// Returns matches for all added rules.
@@ -149,13 +183,6 @@ impl<'db> MatchFinder<'db> {
149 res 183 res
150 } 184 }
151 185
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
159 fn output_debug_for_nodes_at_range( 186 fn output_debug_for_nodes_at_range(
160 &self, 187 &self,
161 node: &SyntaxNode, 188 node: &SyntaxNode,
@@ -175,7 +202,7 @@ impl<'db> MatchFinder<'db> {
175 // we get lots of noise. If at some point we add support for restricting rules 202 // we get lots of noise. If at some point we add support for restricting rules
176 // to a particular kind of thing (e.g. only match type references), then we can 203 // to a particular kind of thing (e.g. only match type references), then we can
177 // relax this. 204 // relax this.
178 if rule.pattern.kind() != node.kind() { 205 if rule.pattern.node.kind() != node.kind() {
179 continue; 206 continue;
180 } 207 }
181 out.push(MatchDebugInfo { 208 out.push(MatchDebugInfo {
@@ -185,7 +212,7 @@ impl<'db> MatchFinder<'db> {
185 "Match failed, but no reason was given".to_owned() 212 "Match failed, but no reason was given".to_owned()
186 }), 213 }),
187 }), 214 }),
188 pattern: rule.pattern.clone(), 215 pattern: rule.pattern.node.clone(),
189 node: node.clone(), 216 node: node.clone(),
190 }); 217 });
191 } 218 }
diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs
index a43d57c34..f3cc60c29 100644
--- a/crates/ra_ssr/src/matching.rs
+++ b/crates/ra_ssr/src/matching.rs
@@ -2,7 +2,8 @@
2//! process of matching, placeholder values are recorded. 2//! process of matching, placeholder values are recorded.
3 3
4use crate::{ 4use crate::{
5 parsing::{Constraint, NodeKind, ParsedRule, Placeholder}, 5 parsing::{Constraint, NodeKind, Placeholder},
6 resolving::{ResolvedPattern, ResolvedRule},
6 SsrMatches, 7 SsrMatches,
7}; 8};
8use hir::Semantics; 9use hir::Semantics;
@@ -51,6 +52,8 @@ pub struct Match {
51 pub(crate) rule_index: usize, 52 pub(crate) rule_index: usize,
52 /// The depth of matched_node. 53 /// The depth of matched_node.
53 pub(crate) depth: usize, 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>,
54} 57}
55 58
56/// Represents a `$var` in an SSR query. 59/// Represents a `$var` in an SSR query.
@@ -86,7 +89,7 @@ pub(crate) struct MatchFailed {
86/// parent module, we don't populate nested matches. 89/// parent module, we don't populate nested matches.
87pub(crate) fn get_match( 90pub(crate) fn get_match(
88 debug_active: bool, 91 debug_active: bool,
89 rule: &ParsedRule, 92 rule: &ResolvedRule,
90 code: &SyntaxNode, 93 code: &SyntaxNode,
91 restrict_range: &Option<FileRange>, 94 restrict_range: &Option<FileRange>,
92 sema: &Semantics<ra_ide_db::RootDatabase>, 95 sema: &Semantics<ra_ide_db::RootDatabase>,
@@ -102,7 +105,7 @@ struct Matcher<'db, 'sema> {
102 /// If any placeholders come from anywhere outside of this range, then the match will be 105 /// If any placeholders come from anywhere outside of this range, then the match will be
103 /// rejected. 106 /// rejected.
104 restrict_range: Option<FileRange>, 107 restrict_range: Option<FileRange>,
105 rule: &'sema ParsedRule, 108 rule: &'sema ResolvedRule,
106} 109}
107 110
108/// Which phase of matching we're currently performing. We do two phases because most attempted 111/// Which phase of matching we're currently performing. We do two phases because most attempted
@@ -117,14 +120,14 @@ enum Phase<'a> {
117 120
118impl<'db, 'sema> Matcher<'db, 'sema> { 121impl<'db, 'sema> Matcher<'db, 'sema> {
119 fn try_match( 122 fn try_match(
120 rule: &ParsedRule, 123 rule: &ResolvedRule,
121 code: &SyntaxNode, 124 code: &SyntaxNode,
122 restrict_range: &Option<FileRange>, 125 restrict_range: &Option<FileRange>,
123 sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>, 126 sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>,
124 ) -> Result<Match, MatchFailed> { 127 ) -> Result<Match, MatchFailed> {
125 let match_state = Matcher { sema, restrict_range: restrict_range.clone(), rule }; 128 let match_state = Matcher { sema, restrict_range: restrict_range.clone(), rule };
126 // First pass at matching, where we check that node types and idents match. 129 // 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)?;
128 match_state.validate_range(&sema.original_range(code))?; 131 match_state.validate_range(&sema.original_range(code))?;
129 let mut the_match = Match { 132 let mut the_match = Match {
130 range: sema.original_range(code), 133 range: sema.original_range(code),
@@ -133,11 +136,19 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
133 ignored_comments: Vec::new(), 136 ignored_comments: Vec::new(),
134 rule_index: rule.index, 137 rule_index: rule.index,
135 depth: 0, 138 depth: 0,
139 rendered_template_paths: FxHashMap::default(),
136 }; 140 };
137 // Second matching pass, where we record placeholder matches, ignored comments and maybe do 141 // Second matching pass, where we record placeholder matches, ignored comments and maybe do
138 // any other more expensive checks that we didn't want to do on the first pass. 142 // 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 )?;
140 the_match.depth = sema.ancestors_with_macros(the_match.matched_node.clone()).count(); 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 }
141 Ok(the_match) 152 Ok(the_match)
142 } 153 }
143 154
@@ -195,6 +206,7 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
195 self.attempt_match_record_field_list(phase, pattern, code) 206 self.attempt_match_record_field_list(phase, pattern, code)
196 } 207 }
197 SyntaxKind::TOKEN_TREE => self.attempt_match_token_tree(phase, pattern, code), 208 SyntaxKind::TOKEN_TREE => self.attempt_match_token_tree(phase, pattern, code),
209 SyntaxKind::PATH => self.attempt_match_path(phase, pattern, code),
198 _ => self.attempt_match_node_children(phase, pattern, code), 210 _ => self.attempt_match_node_children(phase, pattern, code),
199 } 211 }
200 } 212 }
@@ -311,6 +323,64 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
311 Ok(()) 323 Ok(())
312 } 324 }
313 325
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
314 /// We want to allow the records to match in any order, so we have special matching logic for 384 /// We want to allow the records to match in any order, so we have special matching logic for
315 /// them. 385 /// them.
316 fn attempt_match_record_field_list( 386 fn attempt_match_record_field_list(
@@ -449,6 +519,28 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
449 } 519 }
450} 520}
451 521
522impl 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
452impl Phase<'_> { 544impl Phase<'_> {
453 fn next_non_trivial(&mut self, code_it: &mut SyntaxElementChildren) -> Option<SyntaxElement> { 545 fn next_non_trivial(&mut self, code_it: &mut SyntaxElementChildren) -> Option<SyntaxElement> {
454 loop { 546 loop {
@@ -578,7 +670,7 @@ mod tests {
578 670
579 let (db, position) = crate::tests::single_file(input); 671 let (db, position) = crate::tests::single_file(input);
580 let mut match_finder = MatchFinder::in_context(&db, position); 672 let mut match_finder = MatchFinder::in_context(&db, position);
581 match_finder.add_rule(rule); 673 match_finder.add_rule(rule).unwrap();
582 let matches = match_finder.matches(); 674 let matches = match_finder.matches();
583 assert_eq!(matches.matches.len(), 1); 675 assert_eq!(matches.matches.len(), 1);
584 assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)"); 676 assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)");
diff --git a/crates/ra_ssr/src/parsing.rs b/crates/ra_ssr/src/parsing.rs
index cf7fb517f..2d6f4e514 100644
--- a/crates/ra_ssr/src/parsing.rs
+++ b/crates/ra_ssr/src/parsing.rs
@@ -7,7 +7,7 @@
7 7
8use crate::errors::bail; 8use crate::errors::bail;
9use crate::{SsrError, SsrPattern, SsrRule}; 9use crate::{SsrError, SsrPattern, SsrRule};
10use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken, T}; 10use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, T};
11use rustc_hash::{FxHashMap, FxHashSet}; 11use rustc_hash::{FxHashMap, FxHashSet};
12use std::str::FromStr; 12use std::str::FromStr;
13 13
@@ -16,7 +16,6 @@ pub(crate) struct ParsedRule {
16 pub(crate) placeholders_by_stand_in: FxHashMap<SmolStr, Placeholder>, 16 pub(crate) placeholders_by_stand_in: FxHashMap<SmolStr, Placeholder>,
17 pub(crate) pattern: SyntaxNode, 17 pub(crate) pattern: SyntaxNode,
18 pub(crate) template: Option<SyntaxNode>, 18 pub(crate) template: Option<SyntaxNode>,
19 pub(crate) index: usize,
20} 19}
21 20
22#[derive(Debug)] 21#[derive(Debug)]
@@ -93,16 +92,11 @@ impl RuleBuilder {
93 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(), 92 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
94 pattern: pattern.syntax().clone(), 93 pattern: pattern.syntax().clone(),
95 template: Some(template.syntax().clone()), 94 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,
100 }), 95 }),
101 (Ok(pattern), None) => self.rules.push(ParsedRule { 96 (Ok(pattern), None) => self.rules.push(ParsedRule {
102 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(), 97 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
103 pattern: pattern.syntax().clone(), 98 pattern: pattern.syntax().clone(),
104 template: None, 99 template: None,
105 index: 0,
106 }), 100 }),
107 _ => {} 101 _ => {}
108 } 102 }
@@ -171,15 +165,6 @@ impl RawPattern {
171 } 165 }
172} 166}
173 167
174impl 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
183impl FromStr for SsrPattern { 168impl FromStr for SsrPattern {
184 type Err = SsrError; 169 type Err = SsrError;
185 170
diff --git a/crates/ra_ssr/src/replacing.rs b/crates/ra_ssr/src/replacing.rs
index f1c5bdf14..4b3f5509c 100644
--- a/crates/ra_ssr/src/replacing.rs
+++ b/crates/ra_ssr/src/replacing.rs
@@ -1,9 +1,9 @@
1//! Code for applying replacement templates for matches that have previously been found. 1//! Code for applying replacement templates for matches that have previously been found.
2 2
3use crate::matching::Var; 3use crate::matching::Var;
4use crate::{parsing::ParsedRule, Match, SsrMatches}; 4use crate::{resolving::ResolvedRule, Match, SsrMatches};
5use ra_syntax::ast::AstToken; 5use ra_syntax::ast::{self, AstToken};
6use ra_syntax::{SyntaxElement, SyntaxNode, SyntaxToken, TextSize}; 6use ra_syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextSize};
7use ra_text_edit::TextEdit; 7use ra_text_edit::TextEdit;
8 8
9/// Returns a text edit that will replace each match in `matches` with its corresponding replacement 9/// Returns a text edit that will replace each match in `matches` with its corresponding replacement
@@ -12,7 +12,7 @@ use ra_text_edit::TextEdit;
12pub(crate) fn matches_to_edit( 12pub(crate) fn matches_to_edit(
13 matches: &SsrMatches, 13 matches: &SsrMatches,
14 file_src: &str, 14 file_src: &str,
15 rules: &[ParsedRule], 15 rules: &[ResolvedRule],
16) -> TextEdit { 16) -> TextEdit {
17 matches_to_edit_at_offset(matches, file_src, 0.into(), rules) 17 matches_to_edit_at_offset(matches, file_src, 0.into(), rules)
18} 18}
@@ -21,7 +21,7 @@ fn matches_to_edit_at_offset(
21 matches: &SsrMatches, 21 matches: &SsrMatches,
22 file_src: &str, 22 file_src: &str,
23 relative_start: TextSize, 23 relative_start: TextSize,
24 rules: &[ParsedRule], 24 rules: &[ResolvedRule],
25) -> TextEdit { 25) -> TextEdit {
26 let mut edit_builder = ra_text_edit::TextEditBuilder::default(); 26 let mut edit_builder = ra_text_edit::TextEditBuilder::default();
27 for m in &matches.matches { 27 for m in &matches.matches {
@@ -36,11 +36,11 @@ fn matches_to_edit_at_offset(
36struct ReplacementRenderer<'a> { 36struct ReplacementRenderer<'a> {
37 match_info: &'a Match, 37 match_info: &'a Match,
38 file_src: &'a str, 38 file_src: &'a str,
39 rules: &'a [ParsedRule], 39 rules: &'a [ResolvedRule],
40 rule: &'a ParsedRule, 40 rule: &'a ResolvedRule,
41} 41}
42 42
43fn render_replace(match_info: &Match, file_src: &str, rules: &[ParsedRule]) -> String { 43fn render_replace(match_info: &Match, file_src: &str, rules: &[ResolvedRule]) -> String {
44 let mut out = String::new(); 44 let mut out = String::new();
45 let rule = &rules[match_info.rule_index]; 45 let rule = &rules[match_info.rule_index];
46 let template = rule 46 let template = rule
@@ -48,7 +48,7 @@ fn render_replace(match_info: &Match, file_src: &str, rules: &[ParsedRule]) -> S
48 .as_ref() 48 .as_ref()
49 .expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern"); 49 .expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern");
50 let renderer = ReplacementRenderer { match_info, file_src, rules, rule }; 50 let renderer = ReplacementRenderer { match_info, file_src, rules, rule };
51 renderer.render_node_children(&template, &mut out); 51 renderer.render_node(&template.node, &mut out);
52 for comment in &match_info.ignored_comments { 52 for comment in &match_info.ignored_comments {
53 out.push_str(&comment.syntax().to_string()); 53 out.push_str(&comment.syntax().to_string());
54 } 54 }
@@ -68,11 +68,31 @@ impl ReplacementRenderer<'_> {
68 self.render_token(&token, out); 68 self.render_token(&token, out);
69 } 69 }
70 SyntaxElement::Node(child_node) => { 70 SyntaxElement::Node(child_node) => {
71 self.render_node_children(&child_node, out); 71 self.render_node(&child_node, out);
72 } 72 }
73 } 73 }
74 } 74 }
75 75
76 fn render_node(&self, node: &SyntaxNode, out: &mut String) {
77 use ra_syntax::ast::AstNode;
78 if let Some(mod_path) = self.match_info.rendered_template_paths.get(&node) {
79 out.push_str(&mod_path.to_string());
80 // Emit everything except for the segment's name-ref, since we already effectively
81 // emitted that as part of `mod_path`.
82 if let Some(path) = ast::Path::cast(node.clone()) {
83 if let Some(segment) = path.segment() {
84 for node_or_token in segment.syntax().children_with_tokens() {
85 if node_or_token.kind() != SyntaxKind::NAME_REF {
86 self.render_node_or_token(&node_or_token, out);
87 }
88 }
89 }
90 }
91 } else {
92 self.render_node_children(&node, out);
93 }
94 }
95
76 fn render_token(&self, token: &SyntaxToken, out: &mut String) { 96 fn render_token(&self, token: &SyntaxToken, out: &mut String) {
77 if let Some(placeholder) = self.rule.get_placeholder(&token) { 97 if let Some(placeholder) = self.rule.get_placeholder(&token) {
78 if let Some(placeholder_value) = 98 if let Some(placeholder_value) =
diff --git a/crates/ra_ssr/src/resolving.rs b/crates/ra_ssr/src/resolving.rs
new file mode 100644
index 000000000..e9d052111
--- /dev/null
+++ b/crates/ra_ssr/src/resolving.rs
@@ -0,0 +1,153 @@
1//! This module is responsible for resolving paths within rules.
2
3use crate::errors::error;
4use crate::{parsing, SsrError};
5use parsing::Placeholder;
6use ra_syntax::{ast, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken};
7use rustc_hash::{FxHashMap, FxHashSet};
8use test_utils::mark;
9
10pub(crate) struct ResolvedRule {
11 pub(crate) pattern: ResolvedPattern,
12 pub(crate) template: Option<ResolvedPattern>,
13 pub(crate) index: usize,
14}
15
16pub(crate) struct ResolvedPattern {
17 pub(crate) placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>,
18 pub(crate) node: SyntaxNode,
19 // Paths in `node` that we've resolved.
20 pub(crate) resolved_paths: FxHashMap<SyntaxNode, ResolvedPath>,
21}
22
23pub(crate) struct ResolvedPath {
24 pub(crate) resolution: hir::PathResolution,
25}
26
27impl ResolvedRule {
28 pub(crate) fn new(
29 rule: parsing::ParsedRule,
30 scope: &hir::SemanticsScope,
31 hygiene: &hir::Hygiene,
32 index: usize,
33 ) -> Result<ResolvedRule, SsrError> {
34 let resolver =
35 Resolver { scope, hygiene, placeholders_by_stand_in: rule.placeholders_by_stand_in };
36 let resolved_template = if let Some(template) = rule.template {
37 Some(resolver.resolve_pattern_tree(template)?)
38 } else {
39 None
40 };
41 Ok(ResolvedRule {
42 pattern: resolver.resolve_pattern_tree(rule.pattern)?,
43 template: resolved_template,
44 index,
45 })
46 }
47
48 pub(crate) fn get_placeholder(&self, token: &SyntaxToken) -> Option<&Placeholder> {
49 if token.kind() != SyntaxKind::IDENT {
50 return None;
51 }
52 self.pattern.placeholders_by_stand_in.get(token.text())
53 }
54}
55
56struct Resolver<'a, 'db> {
57 scope: &'a hir::SemanticsScope<'db>,
58 hygiene: &'a hir::Hygiene,
59 placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>,
60}
61
62impl Resolver<'_, '_> {
63 fn resolve_pattern_tree(&self, pattern: SyntaxNode) -> Result<ResolvedPattern, SsrError> {
64 let mut resolved_paths = FxHashMap::default();
65 self.resolve(pattern.clone(), &mut resolved_paths)?;
66 Ok(ResolvedPattern {
67 node: pattern,
68 resolved_paths,
69 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
70 })
71 }
72
73 fn resolve(
74 &self,
75 node: SyntaxNode,
76 resolved_paths: &mut FxHashMap<SyntaxNode, ResolvedPath>,
77 ) -> Result<(), SsrError> {
78 use ra_syntax::ast::AstNode;
79 if let Some(path) = ast::Path::cast(node.clone()) {
80 // Check if this is an appropriate place in the path to resolve. If the path is
81 // something like `a::B::<i32>::c` then we want to resolve `a::B`. If the path contains
82 // a placeholder. e.g. `a::$b::c` then we want to resolve `a`.
83 if !path_contains_type_arguments(path.qualifier())
84 && !self.path_contains_placeholder(&path)
85 {
86 let resolution = self
87 .resolve_path(&path)
88 .ok_or_else(|| error!("Failed to resolve path `{}`", node.text()))?;
89 resolved_paths.insert(node, ResolvedPath { resolution });
90 return Ok(());
91 }
92 }
93 for node in node.children() {
94 self.resolve(node, resolved_paths)?;
95 }
96 Ok(())
97 }
98
99 /// Returns whether `path` contains a placeholder, but ignores any placeholders within type
100 /// arguments.
101 fn path_contains_placeholder(&self, path: &ast::Path) -> bool {
102 if let Some(segment) = path.segment() {
103 if let Some(name_ref) = segment.name_ref() {
104 if self.placeholders_by_stand_in.contains_key(name_ref.text()) {
105 return true;
106 }
107 }
108 }
109 if let Some(qualifier) = path.qualifier() {
110 return self.path_contains_placeholder(&qualifier);
111 }
112 false
113 }
114
115 fn resolve_path(&self, path: &ast::Path) -> Option<hir::PathResolution> {
116 let hir_path = hir::Path::from_src(path.clone(), self.hygiene)?;
117 // First try resolving the whole path. This will work for things like
118 // `std::collections::HashMap`, but will fail for things like
119 // `std::collections::HashMap::new`.
120 if let Some(resolution) = self.scope.resolve_hir_path(&hir_path) {
121 return Some(resolution);
122 }
123 // Resolution failed, try resolving the qualifier (e.g. `std::collections::HashMap` and if
124 // that succeeds, then iterate through the candidates on the resolved type with the provided
125 // name.
126 let resolved_qualifier = self.scope.resolve_hir_path_qualifier(&hir_path.qualifier()?)?;
127 if let hir::PathResolution::Def(hir::ModuleDef::Adt(adt)) = resolved_qualifier {
128 adt.ty(self.scope.db).iterate_path_candidates(
129 self.scope.db,
130 self.scope.module()?.krate(),
131 &FxHashSet::default(),
132 Some(hir_path.segments().last()?.name),
133 |_ty, assoc_item| Some(hir::PathResolution::AssocItem(assoc_item)),
134 )
135 } else {
136 None
137 }
138 }
139}
140
141/// Returns whether `path` or any of its qualifiers contains type arguments.
142fn path_contains_type_arguments(path: Option<ast::Path>) -> bool {
143 if let Some(path) = path {
144 if let Some(segment) = path.segment() {
145 if segment.type_arg_list().is_some() {
146 mark::hit!(type_arguments_within_path);
147 return true;
148 }
149 }
150 return path_contains_type_arguments(path.qualifier());
151 }
152 false
153}
diff --git a/crates/ra_ssr/src/search.rs b/crates/ra_ssr/src/search.rs
index a28e9f341..ccc2d544a 100644
--- a/crates/ra_ssr/src/search.rs
+++ b/crates/ra_ssr/src/search.rs
@@ -1,6 +1,6 @@
1//! Searching for matches. 1//! Searching for matches.
2 2
3use crate::{matching, parsing::ParsedRule, Match, MatchFinder}; 3use crate::{matching, resolving::ResolvedRule, Match, MatchFinder};
4use ra_db::FileRange; 4use ra_db::FileRange;
5use ra_syntax::{ast, AstNode, SyntaxNode}; 5use ra_syntax::{ast, AstNode, SyntaxNode};
6 6
@@ -8,13 +8,13 @@ impl<'db> MatchFinder<'db> {
8 /// Adds all matches for `rule` to `matches_out`. Matches may overlap in ways that make 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 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. 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>) { 11 pub(crate) fn find_matches_for_rule(&self, rule: &ResolvedRule, matches_out: &mut Vec<Match>) {
12 // FIXME: Use resolved paths in the pattern to find places to search instead of always 12 // FIXME: Use resolved paths in the pattern to find places to search instead of always
13 // scanning every node. 13 // scanning every node.
14 self.slow_scan(rule, matches_out); 14 self.slow_scan(rule, matches_out);
15 } 15 }
16 16
17 fn slow_scan(&self, rule: &ParsedRule, matches_out: &mut Vec<Match>) { 17 fn slow_scan(&self, rule: &ResolvedRule, matches_out: &mut Vec<Match>) {
18 use ra_db::SourceDatabaseExt; 18 use ra_db::SourceDatabaseExt;
19 use ra_ide_db::symbol_index::SymbolsDatabase; 19 use ra_ide_db::symbol_index::SymbolsDatabase;
20 for &root in self.sema.db.local_roots().iter() { 20 for &root in self.sema.db.local_roots().iter() {
@@ -30,7 +30,7 @@ impl<'db> MatchFinder<'db> {
30 fn slow_scan_node( 30 fn slow_scan_node(
31 &self, 31 &self,
32 code: &SyntaxNode, 32 code: &SyntaxNode,
33 rule: &ParsedRule, 33 rule: &ResolvedRule,
34 restrict_range: &Option<FileRange>, 34 restrict_range: &Option<FileRange>,
35 matches_out: &mut Vec<Match>, 35 matches_out: &mut Vec<Match>,
36 ) { 36 ) {
diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs
index 63d527894..33742dc8e 100644
--- a/crates/ra_ssr/src/tests.rs
+++ b/crates/ra_ssr/src/tests.rs
@@ -85,7 +85,7 @@ fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
85 let mut match_finder = MatchFinder::in_context(&db, position); 85 let mut match_finder = MatchFinder::in_context(&db, position);
86 for rule in rules { 86 for rule in rules {
87 let rule: SsrRule = rule.parse().unwrap(); 87 let rule: SsrRule = rule.parse().unwrap();
88 match_finder.add_rule(rule); 88 match_finder.add_rule(rule).unwrap();
89 } 89 }
90 let edits = match_finder.edits(); 90 let edits = match_finder.edits();
91 if edits.is_empty() { 91 if edits.is_empty() {
@@ -114,7 +114,7 @@ fn print_match_debug_info(match_finder: &MatchFinder, file_id: FileId, snippet:
114fn assert_matches(pattern: &str, code: &str, expected: &[&str]) { 114fn assert_matches(pattern: &str, code: &str, expected: &[&str]) {
115 let (db, position) = single_file(code); 115 let (db, position) = single_file(code);
116 let mut match_finder = MatchFinder::in_context(&db, position); 116 let mut match_finder = MatchFinder::in_context(&db, position);
117 match_finder.add_search_pattern(pattern.parse().unwrap()); 117 match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap();
118 let matched_strings: Vec<String> = 118 let matched_strings: Vec<String> =
119 match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect(); 119 match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect();
120 if matched_strings != expected && !expected.is_empty() { 120 if matched_strings != expected && !expected.is_empty() {
@@ -126,7 +126,7 @@ fn assert_matches(pattern: &str, code: &str, expected: &[&str]) {
126fn assert_no_match(pattern: &str, code: &str) { 126fn assert_no_match(pattern: &str, code: &str) {
127 let (db, position) = single_file(code); 127 let (db, position) = single_file(code);
128 let mut match_finder = MatchFinder::in_context(&db, position); 128 let mut match_finder = MatchFinder::in_context(&db, position);
129 match_finder.add_search_pattern(pattern.parse().unwrap()); 129 match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap();
130 let matches = match_finder.matches().flattened().matches; 130 let matches = match_finder.matches().flattened().matches;
131 if !matches.is_empty() { 131 if !matches.is_empty() {
132 print_match_debug_info(&match_finder, position.file_id, &matches[0].matched_text()); 132 print_match_debug_info(&match_finder, position.file_id, &matches[0].matched_text());
@@ -137,7 +137,7 @@ fn assert_no_match(pattern: &str, code: &str) {
137fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) { 137fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) {
138 let (db, position) = single_file(code); 138 let (db, position) = single_file(code);
139 let mut match_finder = MatchFinder::in_context(&db, position); 139 let mut match_finder = MatchFinder::in_context(&db, position);
140 match_finder.add_search_pattern(pattern.parse().unwrap()); 140 match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap();
141 let mut reasons = Vec::new(); 141 let mut reasons = Vec::new();
142 for d in match_finder.debug_where_text_equal(position.file_id, snippet) { 142 for d in match_finder.debug_where_text_equal(position.file_id, snippet) {
143 if let Some(reason) = d.match_failure_reason() { 143 if let Some(reason) = d.match_failure_reason() {
@@ -350,6 +350,60 @@ fn match_pattern() {
350 assert_matches("Some($a)", "struct Some(); fn f() {if let Some(x) = foo() {}}", &["Some(x)"]); 350 assert_matches("Some($a)", "struct Some(); fn f() {if let Some(x) = foo() {}}", &["Some(x)"]);
351} 351}
352 352
353// If our pattern has a full path, e.g. a::b::c() and the code has c(), but c resolves to
354// a::b::c, then we should match.
355#[test]
356fn match_fully_qualified_fn_path() {
357 let code = r#"
358 mod a {
359 pub mod b {
360 pub fn c(_: i32) {}
361 }
362 }
363 use a::b::c;
364 fn f1() {
365 c(42);
366 }
367 "#;
368 assert_matches("a::b::c($a)", code, &["c(42)"]);
369}
370
371#[test]
372fn match_resolved_type_name() {
373 let code = r#"
374 mod m1 {
375 pub mod m2 {
376 pub trait Foo<T> {}
377 }
378 }
379 mod m3 {
380 trait Foo<T> {}
381 fn f1(f: Option<&dyn Foo<bool>>) {}
382 }
383 mod m4 {
384 use crate::m1::m2::Foo;
385 fn f1(f: Option<&dyn Foo<i32>>) {}
386 }
387 "#;
388 assert_matches("m1::m2::Foo<$t>", code, &["Foo<i32>"]);
389}
390
391#[test]
392fn type_arguments_within_path() {
393 mark::check!(type_arguments_within_path);
394 let code = r#"
395 mod foo {
396 pub struct Bar<T> {t: T}
397 impl<T> Bar<T> {
398 pub fn baz() {}
399 }
400 }
401 fn f1() {foo::Bar::<i32>::baz();}
402 "#;
403 assert_no_match("foo::Bar::<i64>::baz()", code);
404 assert_matches("foo::Bar::<i32>::baz()", code, &["foo::Bar::<i32>::baz()"]);
405}
406
353#[test] 407#[test]
354fn literal_constraint() { 408fn literal_constraint() {
355 mark::check!(literal_constraint); 409 mark::check!(literal_constraint);
@@ -483,6 +537,86 @@ fn replace_associated_function_call() {
483} 537}
484 538
485#[test] 539#[test]
540fn replace_path_in_different_contexts() {
541 // Note the <|> inside module a::b which marks the point where the rule is interpreted. We
542 // replace foo with bar, but both need different path qualifiers in different contexts. In f4,
543 // foo is unqualified because of a use statement, however the replacement needs to be fully
544 // qualified.
545 assert_ssr_transform(
546 "c::foo() ==>> c::bar()",
547 r#"
548 mod a {
549 pub mod b {<|>
550 pub mod c {
551 pub fn foo() {}
552 pub fn bar() {}
553 fn f1() { foo() }
554 }
555 fn f2() { c::foo() }
556 }
557 fn f3() { b::c::foo() }
558 }
559 use a::b::c::foo;
560 fn f4() { foo() }
561 "#,
562 expect![[r#"
563 mod a {
564 pub mod b {
565 pub mod c {
566 pub fn foo() {}
567 pub fn bar() {}
568 fn f1() { bar() }
569 }
570 fn f2() { c::bar() }
571 }
572 fn f3() { b::c::bar() }
573 }
574 use a::b::c::foo;
575 fn f4() { a::b::c::bar() }
576 "#]],
577 );
578}
579
580#[test]
581fn replace_associated_function_with_generics() {
582 assert_ssr_transform(
583 "c::Foo::<$a>::new() ==>> d::Bar::<$a>::default()",
584 r#"
585 mod c {
586 pub struct Foo<T> {v: T}
587 impl<T> Foo<T> { pub fn new() {} }
588 fn f1() {
589 Foo::<i32>::new();
590 }
591 }
592 mod d {
593 pub struct Bar<T> {v: T}
594 impl<T> Bar<T> { pub fn default() {} }
595 fn f1() {
596 super::c::Foo::<i32>::new();
597 }
598 }
599 "#,
600 expect![[r#"
601 mod c {
602 pub struct Foo<T> {v: T}
603 impl<T> Foo<T> { pub fn new() {} }
604 fn f1() {
605 crate::d::Bar::<i32>::default();
606 }
607 }
608 mod d {
609 pub struct Bar<T> {v: T}
610 impl<T> Bar<T> { pub fn default() {} }
611 fn f1() {
612 Bar::<i32>::default();
613 }
614 }
615 "#]],
616 );
617}
618
619#[test]
486fn replace_type() { 620fn replace_type() {
487 assert_ssr_transform( 621 assert_ssr_transform(
488 "Result<(), $a> ==>> Option<$a>", 622 "Result<(), $a> ==>> Option<$a>",
diff --git a/crates/rust-analyzer/src/cli/ssr.rs b/crates/rust-analyzer/src/cli/ssr.rs
index 22f5b4be0..194bec008 100644
--- a/crates/rust-analyzer/src/cli/ssr.rs
+++ b/crates/rust-analyzer/src/cli/ssr.rs
@@ -9,7 +9,7 @@ pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> {
9 let db = host.raw_database(); 9 let db = host.raw_database();
10 let mut match_finder = MatchFinder::at_first_file(db)?; 10 let mut match_finder = MatchFinder::at_first_file(db)?;
11 for rule in rules { 11 for rule in rules {
12 match_finder.add_rule(rule); 12 match_finder.add_rule(rule)?;
13 } 13 }
14 let edits = match_finder.edits(); 14 let edits = match_finder.edits();
15 for edit in edits { 15 for edit in edits {
@@ -32,7 +32,7 @@ pub fn search_for_patterns(patterns: Vec<SsrPattern>, debug_snippet: Option<Stri
32 let db = host.raw_database(); 32 let db = host.raw_database();
33 let mut match_finder = MatchFinder::at_first_file(db)?; 33 let mut match_finder = MatchFinder::at_first_file(db)?;
34 for pattern in patterns { 34 for pattern in patterns {
35 match_finder.add_search_pattern(pattern); 35 match_finder.add_search_pattern(pattern)?;
36 } 36 }
37 if let Some(debug_snippet) = &debug_snippet { 37 if let Some(debug_snippet) = &debug_snippet {
38 for &root in db.local_roots().iter() { 38 for &root in db.local_roots().iter() {