aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Lattimore <[email protected]>2020-07-22 10:15:19 +0100
committerDavid Lattimore <[email protected]>2020-07-24 12:34:00 +0100
commit113abbeefee671266d2d9bebdbd517eb8b802ef8 (patch)
treea7c44c0490b09f06034cb82966b8a61f681c4433
parent1fce8b6ba32bebba36d588d07781e9e578845728 (diff)
SSR: Parse template as Rust code.
This is in preparation for a subsequent commit where we add special handling for paths in the template, allowing them to be qualified differently in different contexts.
-rw-r--r--crates/ra_ssr/src/lib.rs14
-rw-r--r--crates/ra_ssr/src/matching.rs10
-rw-r--r--crates/ra_ssr/src/parsing.rs60
-rw-r--r--crates/ra_ssr/src/replacing.rs106
-rw-r--r--crates/ra_ssr/src/tests.rs4
5 files changed, 112 insertions, 82 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
index 3009dcb93..b28913a65 100644
--- a/crates/ra_ssr/src/lib.rs
+++ b/crates/ra_ssr/src/lib.rs
@@ -15,7 +15,6 @@ pub use crate::errors::SsrError;
15pub use crate::matching::Match; 15pub use crate::matching::Match;
16use crate::matching::MatchFailureReason; 16use crate::matching::MatchFailureReason;
17use hir::Semantics; 17use hir::Semantics;
18use parsing::SsrTemplate;
19use ra_db::{FileId, FileRange}; 18use ra_db::{FileId, FileRange};
20use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; 19use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
21use ra_text_edit::TextEdit; 20use ra_text_edit::TextEdit;
@@ -26,7 +25,7 @@ pub struct SsrRule {
26 /// A structured pattern that we're searching for. 25 /// A structured pattern that we're searching for.
27 pattern: parsing::RawPattern, 26 pattern: parsing::RawPattern,
28 /// What we'll replace it with. 27 /// What we'll replace it with.
29 template: SsrTemplate, 28 template: parsing::RawPattern,
30 parsed_rules: Vec<parsing::ParsedRule>, 29 parsed_rules: Vec<parsing::ParsedRule>,
31} 30}
32 31
@@ -72,7 +71,11 @@ impl<'db> MatchFinder<'db> {
72 None 71 None
73 } else { 72 } else {
74 use ra_db::SourceDatabaseExt; 73 use ra_db::SourceDatabaseExt;
75 Some(replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id))) 74 Some(replacing::matches_to_edit(
75 &matches,
76 &self.sema.db.file_text(file_id),
77 &self.rules,
78 ))
76 } 79 }
77 } 80 }
78 81
@@ -111,9 +114,8 @@ impl<'db> MatchFinder<'db> {
111 } 114 }
112 115
113 fn add_parsed_rules(&mut self, parsed_rules: Vec<parsing::ParsedRule>) { 116 fn add_parsed_rules(&mut self, parsed_rules: Vec<parsing::ParsedRule>) {
114 // FIXME: This doesn't need to be a for loop, but does in a subsequent commit. Justify it 117 for mut parsed_rule in parsed_rules {
115 // being a for-loop. 118 parsed_rule.index = self.rules.len();
116 for parsed_rule in parsed_rules {
117 self.rules.push(parsed_rule); 119 self.rules.push(parsed_rule);
118 } 120 }
119 } 121 }
diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs
index 842f4b6f3..486191635 100644
--- a/crates/ra_ssr/src/matching.rs
+++ b/crates/ra_ssr/src/matching.rs
@@ -2,7 +2,7 @@
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, SsrTemplate}, 5 parsing::{Constraint, NodeKind, ParsedRule, Placeholder},
6 SsrMatches, 6 SsrMatches,
7}; 7};
8use hir::Semantics; 8use hir::Semantics;
@@ -48,9 +48,7 @@ pub struct Match {
48 pub(crate) matched_node: SyntaxNode, 48 pub(crate) matched_node: SyntaxNode,
49 pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>, 49 pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>,
50 pub(crate) ignored_comments: Vec<ast::Comment>, 50 pub(crate) ignored_comments: Vec<ast::Comment>,
51 // A copy of the template for the rule that produced this match. We store this on the match for 51 pub(crate) rule_index: usize,
52 // if/when we do replacement.
53 pub(crate) template: Option<SsrTemplate>,
54} 52}
55 53
56/// Represents a `$var` in an SSR query. 54/// Represents a `$var` in an SSR query.
@@ -131,7 +129,7 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
131 matched_node: code.clone(), 129 matched_node: code.clone(),
132 placeholder_values: FxHashMap::default(), 130 placeholder_values: FxHashMap::default(),
133 ignored_comments: Vec::new(), 131 ignored_comments: Vec::new(),
134 template: rule.template.clone(), 132 rule_index: rule.index,
135 }; 133 };
136 // Second matching pass, where we record placeholder matches, ignored comments and maybe do 134 // Second matching pass, where we record placeholder matches, ignored comments and maybe do
137 // any other more expensive checks that we didn't want to do on the first pass. 135 // any other more expensive checks that we didn't want to do on the first pass.
@@ -591,7 +589,7 @@ mod tests {
591 "1+2" 589 "1+2"
592 ); 590 );
593 591
594 let edit = crate::replacing::matches_to_edit(&matches, input); 592 let edit = crate::replacing::matches_to_edit(&matches, input, &match_finder.rules);
595 let mut after = input.to_string(); 593 let mut after = input.to_string();
596 edit.apply(&mut after); 594 edit.apply(&mut after);
597 assert_eq!(after, "fn foo() {} fn main() { bar(1+2); }"); 595 assert_eq!(after, "fn foo() {} fn main() { bar(1+2); }");
diff --git a/crates/ra_ssr/src/parsing.rs b/crates/ra_ssr/src/parsing.rs
index 682b7011a..cf7fb517f 100644
--- a/crates/ra_ssr/src/parsing.rs
+++ b/crates/ra_ssr/src/parsing.rs
@@ -15,12 +15,8 @@ use std::str::FromStr;
15pub(crate) struct ParsedRule { 15pub(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<SsrTemplate>, 18 pub(crate) template: Option<SyntaxNode>,
19} 19 pub(crate) index: usize,
20
21#[derive(Clone, Debug)]
22pub(crate) struct SsrTemplate {
23 pub(crate) tokens: Vec<PatternElement>,
24} 20}
25 21
26#[derive(Debug)] 22#[derive(Debug)]
@@ -64,18 +60,23 @@ pub(crate) struct Token {
64impl ParsedRule { 60impl ParsedRule {
65 fn new( 61 fn new(
66 pattern: &RawPattern, 62 pattern: &RawPattern,
67 template: Option<&SsrTemplate>, 63 template: Option<&RawPattern>,
68 ) -> Result<Vec<ParsedRule>, SsrError> { 64 ) -> Result<Vec<ParsedRule>, SsrError> {
69 let raw_pattern = pattern.as_rust_code(); 65 let raw_pattern = pattern.as_rust_code();
66 let raw_template = template.map(|t| t.as_rust_code());
67 let raw_template = raw_template.as_ref().map(|s| s.as_str());
70 let mut builder = RuleBuilder { 68 let mut builder = RuleBuilder {
71 placeholders_by_stand_in: pattern.placeholders_by_stand_in(), 69 placeholders_by_stand_in: pattern.placeholders_by_stand_in(),
72 rules: Vec::new(), 70 rules: Vec::new(),
73 }; 71 };
74 builder.try_add(ast::Expr::parse(&raw_pattern), template); 72 builder.try_add(ast::Expr::parse(&raw_pattern), raw_template.map(ast::Expr::parse));
75 builder.try_add(ast::TypeRef::parse(&raw_pattern), template); 73 builder.try_add(ast::TypeRef::parse(&raw_pattern), raw_template.map(ast::TypeRef::parse));
76 builder.try_add(ast::ModuleItem::parse(&raw_pattern), template); 74 builder.try_add(
77 builder.try_add(ast::Path::parse(&raw_pattern), template); 75 ast::ModuleItem::parse(&raw_pattern),
78 builder.try_add(ast::Pat::parse(&raw_pattern), template); 76 raw_template.map(ast::ModuleItem::parse),
77 );
78 builder.try_add(ast::Path::parse(&raw_pattern), raw_template.map(ast::Path::parse));
79 builder.try_add(ast::Pat::parse(&raw_pattern), raw_template.map(ast::Pat::parse));
79 builder.build() 80 builder.build()
80 } 81 }
81} 82}
@@ -86,12 +87,22 @@ struct RuleBuilder {
86} 87}
87 88
88impl RuleBuilder { 89impl RuleBuilder {
89 fn try_add<T: AstNode>(&mut self, pattern: Result<T, ()>, template: Option<&SsrTemplate>) { 90 fn try_add<T: AstNode>(&mut self, pattern: Result<T, ()>, template: Option<Result<T, ()>>) {
90 match pattern { 91 match (pattern, template) {
91 Ok(pattern) => self.rules.push(ParsedRule { 92 (Ok(pattern), Some(Ok(template))) => self.rules.push(ParsedRule {
93 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
94 pattern: pattern.syntax().clone(),
95 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 }),
101 (Ok(pattern), None) => self.rules.push(ParsedRule {
92 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(), 102 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
93 pattern: pattern.syntax().clone(), 103 pattern: pattern.syntax().clone(),
94 template: template.cloned(), 104 template: None,
105 index: 0,
95 }), 106 }),
96 _ => {} 107 _ => {}
97 } 108 }
@@ -99,7 +110,7 @@ impl RuleBuilder {
99 110
100 fn build(self) -> Result<Vec<ParsedRule>, SsrError> { 111 fn build(self) -> Result<Vec<ParsedRule>, SsrError> {
101 if self.rules.is_empty() { 112 if self.rules.is_empty() {
102 bail!("Pattern is not a valid Rust expression, type, item, path or pattern"); 113 bail!("Not a valid Rust expression, type, item, path or pattern");
103 } 114 }
104 Ok(self.rules) 115 Ok(self.rules)
105 } 116 }
@@ -179,21 +190,6 @@ impl FromStr for SsrPattern {
179 } 190 }
180} 191}
181 192
182impl FromStr for SsrTemplate {
183 type Err = SsrError;
184
185 fn from_str(pattern_str: &str) -> Result<SsrTemplate, SsrError> {
186 let tokens = parse_pattern(pattern_str)?;
187 // Validate that the template is a valid fragment of Rust code. We reuse the validation
188 // logic for search patterns since the only thing that differs is the error message.
189 if SsrPattern::from_str(pattern_str).is_err() {
190 bail!("Replacement is not a valid Rust expression, type, item, path or pattern");
191 }
192 // Our actual template needs to preserve whitespace, so we can't reuse `tokens`.
193 Ok(SsrTemplate { tokens })
194 }
195}
196
197/// Returns `pattern_str`, parsed as a search or replace pattern. If `remove_whitespace` is true, 193/// Returns `pattern_str`, parsed as a search or replace pattern. If `remove_whitespace` is true,
198/// then any whitespace tokens will be removed, which we do for the search pattern, but not for the 194/// then any whitespace tokens will be removed, which we do for the search pattern, but not for the
199/// replace pattern. 195/// replace pattern.
diff --git a/crates/ra_ssr/src/replacing.rs b/crates/ra_ssr/src/replacing.rs
index 81f8634ba..f1c5bdf14 100644
--- a/crates/ra_ssr/src/replacing.rs
+++ b/crates/ra_ssr/src/replacing.rs
@@ -1,70 +1,104 @@
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::PatternElement; 4use crate::{parsing::ParsedRule, Match, SsrMatches};
5use crate::{Match, SsrMatches};
6use ra_syntax::ast::AstToken; 5use ra_syntax::ast::AstToken;
7use ra_syntax::TextSize; 6use ra_syntax::{SyntaxElement, SyntaxNode, SyntaxToken, TextSize};
8use ra_text_edit::TextEdit; 7use ra_text_edit::TextEdit;
9 8
10/// 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
11/// template. Placeholders in the template will have been substituted with whatever they matched to 10/// template. Placeholders in the template will have been substituted with whatever they matched to
12/// in the original code. 11/// in the original code.
13pub(crate) fn matches_to_edit(matches: &SsrMatches, file_src: &str) -> TextEdit { 12pub(crate) fn matches_to_edit(
14 matches_to_edit_at_offset(matches, file_src, 0.into()) 13 matches: &SsrMatches,
14 file_src: &str,
15 rules: &[ParsedRule],
16) -> TextEdit {
17 matches_to_edit_at_offset(matches, file_src, 0.into(), rules)
15} 18}
16 19
17fn matches_to_edit_at_offset( 20fn matches_to_edit_at_offset(
18 matches: &SsrMatches, 21 matches: &SsrMatches,
19 file_src: &str, 22 file_src: &str,
20 relative_start: TextSize, 23 relative_start: TextSize,
24 rules: &[ParsedRule],
21) -> TextEdit { 25) -> TextEdit {
22 let mut edit_builder = ra_text_edit::TextEditBuilder::default(); 26 let mut edit_builder = ra_text_edit::TextEditBuilder::default();
23 for m in &matches.matches { 27 for m in &matches.matches {
24 edit_builder.replace( 28 edit_builder.replace(
25 m.range.range.checked_sub(relative_start).unwrap(), 29 m.range.range.checked_sub(relative_start).unwrap(),
26 render_replace(m, file_src), 30 render_replace(m, file_src, rules),
27 ); 31 );
28 } 32 }
29 edit_builder.finish() 33 edit_builder.finish()
30} 34}
31 35
32fn render_replace(match_info: &Match, file_src: &str) -> String { 36struct ReplacementRenderer<'a> {
37 match_info: &'a Match,
38 file_src: &'a str,
39 rules: &'a [ParsedRule],
40 rule: &'a ParsedRule,
41}
42
43fn render_replace(match_info: &Match, file_src: &str, rules: &[ParsedRule]) -> String {
33 let mut out = String::new(); 44 let mut out = String::new();
34 let template = match_info 45 let rule = &rules[match_info.rule_index];
46 let template = rule
35 .template 47 .template
36 .as_ref() 48 .as_ref()
37 .expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern"); 49 .expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern");
38 for r in &template.tokens { 50 let renderer = ReplacementRenderer { match_info, file_src, rules, rule };
39 match r { 51 renderer.render_node_children(&template, &mut out);
40 PatternElement::Token(t) => out.push_str(t.text.as_str()),
41 PatternElement::Placeholder(p) => {
42 if let Some(placeholder_value) =
43 match_info.placeholder_values.get(&Var(p.ident.to_string()))
44 {
45 let range = &placeholder_value.range.range;
46 let mut matched_text =
47 file_src[usize::from(range.start())..usize::from(range.end())].to_owned();
48 let edit = matches_to_edit_at_offset(
49 &placeholder_value.inner_matches,
50 file_src,
51 range.start(),
52 );
53 edit.apply(&mut matched_text);
54 out.push_str(&matched_text);
55 } else {
56 // We validated that all placeholder references were valid before we
57 // started, so this shouldn't happen.
58 panic!(
59 "Internal error: replacement referenced unknown placeholder {}",
60 p.ident
61 );
62 }
63 }
64 }
65 }
66 for comment in &match_info.ignored_comments { 52 for comment in &match_info.ignored_comments {
67 out.push_str(&comment.syntax().to_string()); 53 out.push_str(&comment.syntax().to_string());
68 } 54 }
69 out 55 out
70} 56}
57
58impl ReplacementRenderer<'_> {
59 fn render_node_children(&self, node: &SyntaxNode, out: &mut String) {
60 for node_or_token in node.children_with_tokens() {
61 self.render_node_or_token(&node_or_token, out);
62 }
63 }
64
65 fn render_node_or_token(&self, node_or_token: &SyntaxElement, out: &mut String) {
66 match node_or_token {
67 SyntaxElement::Token(token) => {
68 self.render_token(&token, out);
69 }
70 SyntaxElement::Node(child_node) => {
71 self.render_node_children(&child_node, out);
72 }
73 }
74 }
75
76 fn render_token(&self, token: &SyntaxToken, out: &mut String) {
77 if let Some(placeholder) = self.rule.get_placeholder(&token) {
78 if let Some(placeholder_value) =
79 self.match_info.placeholder_values.get(&Var(placeholder.ident.to_string()))
80 {
81 let range = &placeholder_value.range.range;
82 let mut matched_text =
83 self.file_src[usize::from(range.start())..usize::from(range.end())].to_owned();
84 let edit = matches_to_edit_at_offset(
85 &placeholder_value.inner_matches,
86 self.file_src,
87 range.start(),
88 self.rules,
89 );
90 edit.apply(&mut matched_text);
91 out.push_str(&matched_text);
92 } else {
93 // We validated that all placeholder references were valid before we
94 // started, so this shouldn't happen.
95 panic!(
96 "Internal error: replacement referenced unknown placeholder {}",
97 placeholder.ident
98 );
99 }
100 } else {
101 out.push_str(token.text().as_str());
102 }
103 }
104}
diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs
index 9f5306592..1b03b7f4b 100644
--- a/crates/ra_ssr/src/tests.rs
+++ b/crates/ra_ssr/src/tests.rs
@@ -37,7 +37,7 @@ fn parser_repeated_name() {
37fn parser_invalid_pattern() { 37fn parser_invalid_pattern() {
38 assert_eq!( 38 assert_eq!(
39 parse_error_text(" ==>> ()"), 39 parse_error_text(" ==>> ()"),
40 "Parse error: Pattern is not a valid Rust expression, type, item, path or pattern" 40 "Parse error: Not a valid Rust expression, type, item, path or pattern"
41 ); 41 );
42} 42}
43 43
@@ -45,7 +45,7 @@ fn parser_invalid_pattern() {
45fn parser_invalid_template() { 45fn parser_invalid_template() {
46 assert_eq!( 46 assert_eq!(
47 parse_error_text("() ==>> )"), 47 parse_error_text("() ==>> )"),
48 "Parse error: Replacement is not a valid Rust expression, type, item, path or pattern" 48 "Parse error: Not a valid Rust expression, type, item, path or pattern"
49 ); 49 );
50} 50}
51 51