diff options
author | David Lattimore <[email protected]> | 2020-07-22 10:15:19 +0100 |
---|---|---|
committer | David Lattimore <[email protected]> | 2020-07-24 12:34:00 +0100 |
commit | 113abbeefee671266d2d9bebdbd517eb8b802ef8 (patch) | |
tree | a7c44c0490b09f06034cb82966b8a61f681c4433 | |
parent | 1fce8b6ba32bebba36d588d07781e9e578845728 (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.rs | 14 | ||||
-rw-r--r-- | crates/ra_ssr/src/matching.rs | 10 | ||||
-rw-r--r-- | crates/ra_ssr/src/parsing.rs | 60 | ||||
-rw-r--r-- | crates/ra_ssr/src/replacing.rs | 106 | ||||
-rw-r--r-- | crates/ra_ssr/src/tests.rs | 4 |
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; | |||
15 | pub use crate::matching::Match; | 15 | pub use crate::matching::Match; |
16 | use crate::matching::MatchFailureReason; | 16 | use crate::matching::MatchFailureReason; |
17 | use hir::Semantics; | 17 | use hir::Semantics; |
18 | use parsing::SsrTemplate; | ||
19 | use ra_db::{FileId, FileRange}; | 18 | use ra_db::{FileId, FileRange}; |
20 | use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; | 19 | use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; |
21 | use ra_text_edit::TextEdit; | 20 | use 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 | ||
4 | use crate::{ | 4 | use crate::{ |
5 | parsing::{Constraint, NodeKind, ParsedRule, Placeholder, SsrTemplate}, | 5 | parsing::{Constraint, NodeKind, ParsedRule, Placeholder}, |
6 | SsrMatches, | 6 | SsrMatches, |
7 | }; | 7 | }; |
8 | use hir::Semantics; | 8 | use 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; | |||
15 | pub(crate) struct ParsedRule { | 15 | 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<SsrTemplate>, | 18 | pub(crate) template: Option<SyntaxNode>, |
19 | } | 19 | pub(crate) index: usize, |
20 | |||
21 | #[derive(Clone, Debug)] | ||
22 | pub(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 { | |||
64 | impl ParsedRule { | 60 | impl 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 | ||
88 | impl RuleBuilder { | 89 | impl 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 | ||
182 | impl 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 | ||
3 | use crate::matching::Var; | 3 | use crate::matching::Var; |
4 | use crate::parsing::PatternElement; | 4 | use crate::{parsing::ParsedRule, Match, SsrMatches}; |
5 | use crate::{Match, SsrMatches}; | ||
6 | use ra_syntax::ast::AstToken; | 5 | use ra_syntax::ast::AstToken; |
7 | use ra_syntax::TextSize; | 6 | use ra_syntax::{SyntaxElement, SyntaxNode, SyntaxToken, TextSize}; |
8 | use ra_text_edit::TextEdit; | 7 | use 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. |
13 | pub(crate) fn matches_to_edit(matches: &SsrMatches, file_src: &str) -> TextEdit { | 12 | pub(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 | ||
17 | fn matches_to_edit_at_offset( | 20 | fn 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 | ||
32 | fn render_replace(match_info: &Match, file_src: &str) -> String { | 36 | struct ReplacementRenderer<'a> { |
37 | match_info: &'a Match, | ||
38 | file_src: &'a str, | ||
39 | rules: &'a [ParsedRule], | ||
40 | rule: &'a ParsedRule, | ||
41 | } | ||
42 | |||
43 | fn 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 | |||
58 | impl 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() { | |||
37 | fn parser_invalid_pattern() { | 37 | fn 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() { | |||
45 | fn parser_invalid_template() { | 45 | fn 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 | ||