From 113abbeefee671266d2d9bebdbd517eb8b802ef8 Mon Sep 17 00:00:00 2001 From: David Lattimore Date: Wed, 22 Jul 2020 19:15:19 +1000 Subject: [PATCH] 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. --- crates/ra_ssr/src/lib.rs | 14 +++-- crates/ra_ssr/src/matching.rs | 10 ++-- crates/ra_ssr/src/parsing.rs | 60 +++++++++---------- crates/ra_ssr/src/replacing.rs | 106 ++++++++++++++++++++++----------- 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 3009dcb93b..b28913a650 100644 --- a/crates/ra_ssr/src/lib.rs +++ b/crates/ra_ssr/src/lib.rs @@ -15,7 +15,6 @@ pub use crate::errors::SsrError; pub use crate::matching::Match; use crate::matching::MatchFailureReason; use hir::Semantics; -use parsing::SsrTemplate; use ra_db::{FileId, FileRange}; use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; use ra_text_edit::TextEdit; @@ -26,7 +25,7 @@ pub struct SsrRule { /// A structured pattern that we're searching for. pattern: parsing::RawPattern, /// What we'll replace it with. - template: SsrTemplate, + template: parsing::RawPattern, parsed_rules: Vec, } @@ -72,7 +71,11 @@ impl<'db> MatchFinder<'db> { None } else { use ra_db::SourceDatabaseExt; - Some(replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id))) + Some(replacing::matches_to_edit( + &matches, + &self.sema.db.file_text(file_id), + &self.rules, + )) } } @@ -111,9 +114,8 @@ impl<'db> MatchFinder<'db> { } fn add_parsed_rules(&mut self, parsed_rules: Vec) { - // FIXME: This doesn't need to be a for loop, but does in a subsequent commit. Justify it - // being a for-loop. - for parsed_rule in parsed_rules { + for mut parsed_rule in parsed_rules { + parsed_rule.index = self.rules.len(); self.rules.push(parsed_rule); } } diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs index 842f4b6f35..486191635d 100644 --- a/crates/ra_ssr/src/matching.rs +++ b/crates/ra_ssr/src/matching.rs @@ -2,7 +2,7 @@ //! process of matching, placeholder values are recorded. use crate::{ - parsing::{Constraint, NodeKind, ParsedRule, Placeholder, SsrTemplate}, + parsing::{Constraint, NodeKind, ParsedRule, Placeholder}, SsrMatches, }; use hir::Semantics; @@ -48,9 +48,7 @@ pub struct Match { pub(crate) matched_node: SyntaxNode, pub(crate) placeholder_values: FxHashMap, pub(crate) ignored_comments: Vec, - // A copy of the template for the rule that produced this match. We store this on the match for - // if/when we do replacement. - pub(crate) template: Option, + pub(crate) rule_index: usize, } /// Represents a `$var` in an SSR query. @@ -131,7 +129,7 @@ impl<'db, 'sema> Matcher<'db, 'sema> { matched_node: code.clone(), placeholder_values: FxHashMap::default(), ignored_comments: Vec::new(), - template: rule.template.clone(), + rule_index: rule.index, }; // Second matching pass, where we record placeholder matches, ignored comments and maybe do // any other more expensive checks that we didn't want to do on the first pass. @@ -591,7 +589,7 @@ mod tests { "1+2" ); - let edit = crate::replacing::matches_to_edit(&matches, input); + let edit = crate::replacing::matches_to_edit(&matches, input, &match_finder.rules); let mut after = input.to_string(); edit.apply(&mut after); 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 682b7011a0..cf7fb517fa 100644 --- a/crates/ra_ssr/src/parsing.rs +++ b/crates/ra_ssr/src/parsing.rs @@ -15,12 +15,8 @@ use std::str::FromStr; pub(crate) struct ParsedRule { pub(crate) placeholders_by_stand_in: FxHashMap, pub(crate) pattern: SyntaxNode, - pub(crate) template: Option, -} - -#[derive(Clone, Debug)] -pub(crate) struct SsrTemplate { - pub(crate) tokens: Vec, + pub(crate) template: Option, + pub(crate) index: usize, } #[derive(Debug)] @@ -64,18 +60,23 @@ pub(crate) struct Token { impl ParsedRule { fn new( pattern: &RawPattern, - template: Option<&SsrTemplate>, + template: Option<&RawPattern>, ) -> Result, SsrError> { let raw_pattern = pattern.as_rust_code(); + let raw_template = template.map(|t| t.as_rust_code()); + let raw_template = raw_template.as_ref().map(|s| s.as_str()); let mut builder = RuleBuilder { placeholders_by_stand_in: pattern.placeholders_by_stand_in(), rules: Vec::new(), }; - builder.try_add(ast::Expr::parse(&raw_pattern), template); - builder.try_add(ast::TypeRef::parse(&raw_pattern), template); - builder.try_add(ast::ModuleItem::parse(&raw_pattern), template); - builder.try_add(ast::Path::parse(&raw_pattern), template); - builder.try_add(ast::Pat::parse(&raw_pattern), template); + builder.try_add(ast::Expr::parse(&raw_pattern), raw_template.map(ast::Expr::parse)); + builder.try_add(ast::TypeRef::parse(&raw_pattern), raw_template.map(ast::TypeRef::parse)); + builder.try_add( + ast::ModuleItem::parse(&raw_pattern), + raw_template.map(ast::ModuleItem::parse), + ); + builder.try_add(ast::Path::parse(&raw_pattern), raw_template.map(ast::Path::parse)); + builder.try_add(ast::Pat::parse(&raw_pattern), raw_template.map(ast::Pat::parse)); builder.build() } } @@ -86,12 +87,22 @@ struct RuleBuilder { } impl RuleBuilder { - fn try_add(&mut self, pattern: Result, template: Option<&SsrTemplate>) { - match pattern { - Ok(pattern) => self.rules.push(ParsedRule { + fn try_add(&mut self, pattern: Result, template: Option>) { + match (pattern, template) { + (Ok(pattern), Some(Ok(template))) => self.rules.push(ParsedRule { placeholders_by_stand_in: self.placeholders_by_stand_in.clone(), pattern: pattern.syntax().clone(), - template: template.cloned(), + template: Some(template.syntax().clone()), + // For now we give the rule an index of 0. It's given a proper index when the rule + // is added to the SsrMatcher. Using an Option, instead would be slightly + // more correct, but we delete this field from ParsedRule in a subsequent commit. + index: 0, + }), + (Ok(pattern), None) => self.rules.push(ParsedRule { + placeholders_by_stand_in: self.placeholders_by_stand_in.clone(), + pattern: pattern.syntax().clone(), + template: None, + index: 0, }), _ => {} } @@ -99,7 +110,7 @@ impl RuleBuilder { fn build(self) -> Result, SsrError> { if self.rules.is_empty() { - bail!("Pattern is not a valid Rust expression, type, item, path or pattern"); + bail!("Not a valid Rust expression, type, item, path or pattern"); } Ok(self.rules) } @@ -179,21 +190,6 @@ impl FromStr for SsrPattern { } } -impl FromStr for SsrTemplate { - type Err = SsrError; - - fn from_str(pattern_str: &str) -> Result { - let tokens = parse_pattern(pattern_str)?; - // Validate that the template is a valid fragment of Rust code. We reuse the validation - // logic for search patterns since the only thing that differs is the error message. - if SsrPattern::from_str(pattern_str).is_err() { - bail!("Replacement is not a valid Rust expression, type, item, path or pattern"); - } - // Our actual template needs to preserve whitespace, so we can't reuse `tokens`. - Ok(SsrTemplate { tokens }) - } -} - /// Returns `pattern_str`, parsed as a search or replace pattern. If `remove_whitespace` is true, /// then any whitespace tokens will be removed, which we do for the search pattern, but not for the /// replace pattern. diff --git a/crates/ra_ssr/src/replacing.rs b/crates/ra_ssr/src/replacing.rs index 81f8634baa..f1c5bdf14e 100644 --- a/crates/ra_ssr/src/replacing.rs +++ b/crates/ra_ssr/src/replacing.rs @@ -1,70 +1,104 @@ //! Code for applying replacement templates for matches that have previously been found. use crate::matching::Var; -use crate::parsing::PatternElement; -use crate::{Match, SsrMatches}; +use crate::{parsing::ParsedRule, Match, SsrMatches}; use ra_syntax::ast::AstToken; -use ra_syntax::TextSize; +use ra_syntax::{SyntaxElement, SyntaxNode, SyntaxToken, TextSize}; use ra_text_edit::TextEdit; /// Returns a text edit that will replace each match in `matches` with its corresponding replacement /// template. Placeholders in the template will have been substituted with whatever they matched to /// in the original code. -pub(crate) fn matches_to_edit(matches: &SsrMatches, file_src: &str) -> TextEdit { - matches_to_edit_at_offset(matches, file_src, 0.into()) +pub(crate) fn matches_to_edit( + matches: &SsrMatches, + file_src: &str, + rules: &[ParsedRule], +) -> TextEdit { + matches_to_edit_at_offset(matches, file_src, 0.into(), rules) } fn matches_to_edit_at_offset( matches: &SsrMatches, file_src: &str, relative_start: TextSize, + rules: &[ParsedRule], ) -> TextEdit { let mut edit_builder = ra_text_edit::TextEditBuilder::default(); for m in &matches.matches { edit_builder.replace( m.range.range.checked_sub(relative_start).unwrap(), - render_replace(m, file_src), + render_replace(m, file_src, rules), ); } edit_builder.finish() } -fn render_replace(match_info: &Match, file_src: &str) -> String { +struct ReplacementRenderer<'a> { + match_info: &'a Match, + file_src: &'a str, + rules: &'a [ParsedRule], + rule: &'a ParsedRule, +} + +fn render_replace(match_info: &Match, file_src: &str, rules: &[ParsedRule]) -> String { let mut out = String::new(); - let template = match_info + let rule = &rules[match_info.rule_index]; + let template = rule .template .as_ref() .expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern"); - for r in &template.tokens { - match r { - PatternElement::Token(t) => out.push_str(t.text.as_str()), - PatternElement::Placeholder(p) => { - if let Some(placeholder_value) = - match_info.placeholder_values.get(&Var(p.ident.to_string())) - { - let range = &placeholder_value.range.range; - let mut matched_text = - file_src[usize::from(range.start())..usize::from(range.end())].to_owned(); - let edit = matches_to_edit_at_offset( - &placeholder_value.inner_matches, - file_src, - range.start(), - ); - edit.apply(&mut matched_text); - out.push_str(&matched_text); - } else { - // We validated that all placeholder references were valid before we - // started, so this shouldn't happen. - panic!( - "Internal error: replacement referenced unknown placeholder {}", - p.ident - ); - } - } - } - } + let renderer = ReplacementRenderer { match_info, file_src, rules, rule }; + renderer.render_node_children(&template, &mut out); for comment in &match_info.ignored_comments { out.push_str(&comment.syntax().to_string()); } out } + +impl ReplacementRenderer<'_> { + fn render_node_children(&self, node: &SyntaxNode, out: &mut String) { + for node_or_token in node.children_with_tokens() { + self.render_node_or_token(&node_or_token, out); + } + } + + fn render_node_or_token(&self, node_or_token: &SyntaxElement, out: &mut String) { + match node_or_token { + SyntaxElement::Token(token) => { + self.render_token(&token, out); + } + SyntaxElement::Node(child_node) => { + self.render_node_children(&child_node, out); + } + } + } + + fn render_token(&self, token: &SyntaxToken, out: &mut String) { + if let Some(placeholder) = self.rule.get_placeholder(&token) { + if let Some(placeholder_value) = + self.match_info.placeholder_values.get(&Var(placeholder.ident.to_string())) + { + let range = &placeholder_value.range.range; + let mut matched_text = + self.file_src[usize::from(range.start())..usize::from(range.end())].to_owned(); + let edit = matches_to_edit_at_offset( + &placeholder_value.inner_matches, + self.file_src, + range.start(), + self.rules, + ); + edit.apply(&mut matched_text); + out.push_str(&matched_text); + } else { + // We validated that all placeholder references were valid before we + // started, so this shouldn't happen. + panic!( + "Internal error: replacement referenced unknown placeholder {}", + placeholder.ident + ); + } + } else { + out.push_str(token.text().as_str()); + } + } +} diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs index 9f53065929..1b03b7f4bb 100644 --- a/crates/ra_ssr/src/tests.rs +++ b/crates/ra_ssr/src/tests.rs @@ -37,7 +37,7 @@ fn parser_repeated_name() { fn parser_invalid_pattern() { assert_eq!( parse_error_text(" ==>> ()"), - "Parse error: Pattern is not a valid Rust expression, type, item, path or pattern" + "Parse error: Not a valid Rust expression, type, item, path or pattern" ); } @@ -45,7 +45,7 @@ fn parser_invalid_pattern() { fn parser_invalid_template() { assert_eq!( parse_error_text("() ==>> )"), - "Parse error: Replacement is not a valid Rust expression, type, item, path or pattern" + "Parse error: Not a valid Rust expression, type, item, path or pattern" ); }