From 757f755c29e041fd319af466d7d0418f54cb090a Mon Sep 17 00:00:00 2001 From: David Lattimore Date: Wed, 22 Jul 2020 16:46:29 +1000 Subject: [PATCH] SSR: Match paths based on what they resolve to Also render template paths appropriately for their context. --- crates/ra_ide/src/ssr.rs | 12 ++- crates/ra_ssr/src/lib.rs | 61 +++++++---- crates/ra_ssr/src/matching.rs | 106 +++++++++++++++++-- crates/ra_ssr/src/parsing.rs | 17 +--- crates/ra_ssr/src/replacing.rs | 40 ++++++-- crates/ra_ssr/src/resolving.rs | 153 ++++++++++++++++++++++++++++ crates/ra_ssr/src/search.rs | 8 +- crates/ra_ssr/src/tests.rs | 142 +++++++++++++++++++++++++- crates/rust-analyzer/src/cli/ssr.rs | 4 +- 9 files changed, 482 insertions(+), 61 deletions(-) create mode 100644 crates/ra_ssr/src/resolving.rs diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs index 3e2705d62b..2f40bac08b 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}; // A `$` placeholder in the search pattern will match any AST node and `$` will reference it in the replacement. // Within a macro call, a placeholder will match up until whatever token follows the placeholder. // +// All paths in both the search pattern and the replacement template must resolve in the context +// in which this command is invoked. Paths in the search pattern will then match the code if they +// resolve to the same item, even if they're written differently. For example if we invoke the +// command in the module `foo` with a pattern of `Bar`, then code in the parent module that refers +// to `foo::Bar` will match. +// +// Paths in the replacement template will be rendered appropriately for the context in which the +// replacement occurs. For example if our replacement template is `foo::Bar` and we match some +// code in the `foo` module, we'll insert just `Bar`. +// // Placeholders may be given constraints by writing them as `${::...}`. // // Supported constraints: @@ -47,7 +57,7 @@ pub fn parse_search_replace( ) -> Result, SsrError> { let rule: SsrRule = rule.parse()?; let mut match_finder = MatchFinder::in_context(db, position); - match_finder.add_rule(rule); + match_finder.add_rule(rule)?; if parse_only { return Ok(Vec::new()); } diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs index a0a5c97627..286619f59a 100644 --- a/crates/ra_ssr/src/lib.rs +++ b/crates/ra_ssr/src/lib.rs @@ -7,6 +7,7 @@ mod matching; mod nester; mod parsing; mod replacing; +mod resolving; mod search; #[macro_use] mod errors; @@ -21,6 +22,7 @@ use hir::Semantics; use ra_db::{FileId, FilePosition, FileRange}; use ra_ide_db::source_change::SourceFileEdit; use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; +use resolving::ResolvedRule; use rustc_hash::FxHashMap; // A structured search replace rule. Create by calling `parse` on a str. @@ -48,7 +50,9 @@ pub struct SsrMatches { pub struct MatchFinder<'db> { /// Our source of information about the user's code. sema: Semantics<'db, ra_ide_db::RootDatabase>, - rules: Vec, + rules: Vec, + scope: hir::SemanticsScope<'db>, + hygiene: hir::Hygiene, } impl<'db> MatchFinder<'db> { @@ -56,10 +60,24 @@ impl<'db> MatchFinder<'db> { /// `lookup_context`. pub fn in_context( db: &'db ra_ide_db::RootDatabase, - _lookup_context: FilePosition, + lookup_context: FilePosition, ) -> MatchFinder<'db> { - // FIXME: Use lookup_context - MatchFinder { sema: Semantics::new(db), rules: Vec::new() } + let sema = Semantics::new(db); + let file = sema.parse(lookup_context.file_id); + // Find a node at the requested position, falling back to the whole file. + let node = file + .syntax() + .token_at_offset(lookup_context.offset) + .left_biased() + .map(|token| token.parent()) + .unwrap_or_else(|| file.syntax().clone()); + let scope = sema.scope(&node); + MatchFinder { + sema: Semantics::new(db), + rules: Vec::new(), + scope, + hygiene: hir::Hygiene::new(db, lookup_context.file_id.into()), + } } /// Constructs an instance using the start of the first file in `db` as the lookup context. @@ -84,8 +102,16 @@ impl<'db> MatchFinder<'db> { /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to /// match to it. - pub fn add_rule(&mut self, rule: SsrRule) { - self.add_parsed_rules(rule.parsed_rules); + pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> { + for parsed_rule in rule.parsed_rules { + self.rules.push(ResolvedRule::new( + parsed_rule, + &self.scope, + &self.hygiene, + self.rules.len(), + )?); + } + Ok(()) } /// Finds matches for all added rules and returns edits for all found matches. @@ -110,8 +136,16 @@ impl<'db> MatchFinder<'db> { /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you /// intend to do replacement, use `add_rule` instead. - pub fn add_search_pattern(&mut self, pattern: SsrPattern) { - self.add_parsed_rules(pattern.parsed_rules); + pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> { + for parsed_rule in pattern.parsed_rules { + self.rules.push(ResolvedRule::new( + parsed_rule, + &self.scope, + &self.hygiene, + self.rules.len(), + )?); + } + Ok(()) } /// Returns matches for all added rules. @@ -149,13 +183,6 @@ impl<'db> MatchFinder<'db> { res } - fn add_parsed_rules(&mut self, parsed_rules: Vec) { - for mut parsed_rule in parsed_rules { - parsed_rule.index = self.rules.len(); - self.rules.push(parsed_rule); - } - } - fn output_debug_for_nodes_at_range( &self, node: &SyntaxNode, @@ -175,7 +202,7 @@ impl<'db> MatchFinder<'db> { // we get lots of noise. If at some point we add support for restricting rules // to a particular kind of thing (e.g. only match type references), then we can // relax this. - if rule.pattern.kind() != node.kind() { + if rule.pattern.node.kind() != node.kind() { continue; } out.push(MatchDebugInfo { @@ -185,7 +212,7 @@ impl<'db> MatchFinder<'db> { "Match failed, but no reason was given".to_owned() }), }), - pattern: rule.pattern.clone(), + pattern: rule.pattern.node.clone(), node: node.clone(), }); } diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs index a43d57c342..f3cc60c29d 100644 --- a/crates/ra_ssr/src/matching.rs +++ b/crates/ra_ssr/src/matching.rs @@ -2,7 +2,8 @@ //! process of matching, placeholder values are recorded. use crate::{ - parsing::{Constraint, NodeKind, ParsedRule, Placeholder}, + parsing::{Constraint, NodeKind, Placeholder}, + resolving::{ResolvedPattern, ResolvedRule}, SsrMatches, }; use hir::Semantics; @@ -51,6 +52,8 @@ pub struct Match { pub(crate) rule_index: usize, /// The depth of matched_node. pub(crate) depth: usize, + // Each path in the template rendered for the module in which the match was found. + pub(crate) rendered_template_paths: FxHashMap, } /// Represents a `$var` in an SSR query. @@ -86,7 +89,7 @@ pub(crate) struct MatchFailed { /// parent module, we don't populate nested matches. pub(crate) fn get_match( debug_active: bool, - rule: &ParsedRule, + rule: &ResolvedRule, code: &SyntaxNode, restrict_range: &Option, sema: &Semantics, @@ -102,7 +105,7 @@ struct Matcher<'db, 'sema> { /// If any placeholders come from anywhere outside of this range, then the match will be /// rejected. restrict_range: Option, - rule: &'sema ParsedRule, + rule: &'sema ResolvedRule, } /// Which phase of matching we're currently performing. We do two phases because most attempted @@ -117,14 +120,14 @@ enum Phase<'a> { impl<'db, 'sema> Matcher<'db, 'sema> { fn try_match( - rule: &ParsedRule, + rule: &ResolvedRule, code: &SyntaxNode, restrict_range: &Option, sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>, ) -> Result { let match_state = Matcher { sema, restrict_range: restrict_range.clone(), rule }; // First pass at matching, where we check that node types and idents match. - match_state.attempt_match_node(&mut Phase::First, &rule.pattern, code)?; + match_state.attempt_match_node(&mut Phase::First, &rule.pattern.node, code)?; match_state.validate_range(&sema.original_range(code))?; let mut the_match = Match { range: sema.original_range(code), @@ -133,11 +136,19 @@ impl<'db, 'sema> Matcher<'db, 'sema> { ignored_comments: Vec::new(), rule_index: rule.index, depth: 0, + rendered_template_paths: FxHashMap::default(), }; // 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. - match_state.attempt_match_node(&mut Phase::Second(&mut the_match), &rule.pattern, code)?; + match_state.attempt_match_node( + &mut Phase::Second(&mut the_match), + &rule.pattern.node, + code, + )?; the_match.depth = sema.ancestors_with_macros(the_match.matched_node.clone()).count(); + if let Some(template) = &rule.template { + the_match.render_template_paths(template, sema)?; + } Ok(the_match) } @@ -195,6 +206,7 @@ impl<'db, 'sema> Matcher<'db, 'sema> { self.attempt_match_record_field_list(phase, pattern, code) } SyntaxKind::TOKEN_TREE => self.attempt_match_token_tree(phase, pattern, code), + SyntaxKind::PATH => self.attempt_match_path(phase, pattern, code), _ => self.attempt_match_node_children(phase, pattern, code), } } @@ -311,6 +323,64 @@ impl<'db, 'sema> Matcher<'db, 'sema> { Ok(()) } + /// Paths are matched based on whether they refer to the same thing, even if they're written + /// differently. + fn attempt_match_path( + &self, + phase: &mut Phase, + pattern: &SyntaxNode, + code: &SyntaxNode, + ) -> Result<(), MatchFailed> { + if let Some(pattern_resolved) = self.rule.pattern.resolved_paths.get(pattern) { + let pattern_path = ast::Path::cast(pattern.clone()).unwrap(); + let code_path = ast::Path::cast(code.clone()).unwrap(); + if let (Some(pattern_segment), Some(code_segment)) = + (pattern_path.segment(), code_path.segment()) + { + // Match everything within the segment except for the name-ref, which is handled + // separately via comparing what the path resolves to below. + self.attempt_match_opt( + phase, + pattern_segment.type_arg_list(), + code_segment.type_arg_list(), + )?; + self.attempt_match_opt( + phase, + pattern_segment.param_list(), + code_segment.param_list(), + )?; + } + if matches!(phase, Phase::Second(_)) { + let resolution = self + .sema + .resolve_path(&code_path) + .ok_or_else(|| match_error!("Failed to resolve path `{}`", code.text()))?; + if pattern_resolved.resolution != resolution { + fail_match!("Pattern had path `{}` code had `{}`", pattern.text(), code.text()); + } + } + } else { + return self.attempt_match_node_children(phase, pattern, code); + } + Ok(()) + } + + fn attempt_match_opt( + &self, + phase: &mut Phase, + pattern: Option, + code: Option, + ) -> Result<(), MatchFailed> { + match (pattern, code) { + (Some(p), Some(c)) => self.attempt_match_node(phase, &p.syntax(), &c.syntax()), + (None, None) => Ok(()), + (Some(p), None) => fail_match!("Pattern `{}` had nothing to match", p.syntax().text()), + (None, Some(c)) => { + fail_match!("Nothing in pattern to match code `{}`", c.syntax().text()) + } + } + } + /// We want to allow the records to match in any order, so we have special matching logic for /// them. fn attempt_match_record_field_list( @@ -449,6 +519,28 @@ impl<'db, 'sema> Matcher<'db, 'sema> { } } +impl Match { + fn render_template_paths( + &mut self, + template: &ResolvedPattern, + sema: &Semantics, + ) -> Result<(), MatchFailed> { + let module = sema + .scope(&self.matched_node) + .module() + .ok_or_else(|| match_error!("Matched node isn't in a module"))?; + for (path, resolved_path) in &template.resolved_paths { + if let hir::PathResolution::Def(module_def) = resolved_path.resolution { + let mod_path = module.find_use_path(sema.db, module_def).ok_or_else(|| { + match_error!("Failed to render template path `{}` at match location") + })?; + self.rendered_template_paths.insert(path.clone(), mod_path); + } + } + Ok(()) + } +} + impl Phase<'_> { fn next_non_trivial(&mut self, code_it: &mut SyntaxElementChildren) -> Option { loop { @@ -578,7 +670,7 @@ mod tests { let (db, position) = crate::tests::single_file(input); let mut match_finder = MatchFinder::in_context(&db, position); - match_finder.add_rule(rule); + match_finder.add_rule(rule).unwrap(); let matches = match_finder.matches(); assert_eq!(matches.matches.len(), 1); 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 cf7fb517fa..2d6f4e514f 100644 --- a/crates/ra_ssr/src/parsing.rs +++ b/crates/ra_ssr/src/parsing.rs @@ -7,7 +7,7 @@ use crate::errors::bail; use crate::{SsrError, SsrPattern, SsrRule}; -use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken, T}; +use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, T}; use rustc_hash::{FxHashMap, FxHashSet}; use std::str::FromStr; @@ -16,7 +16,6 @@ pub(crate) struct ParsedRule { pub(crate) placeholders_by_stand_in: FxHashMap, pub(crate) pattern: SyntaxNode, pub(crate) template: Option, - pub(crate) index: usize, } #[derive(Debug)] @@ -93,16 +92,11 @@ impl RuleBuilder { placeholders_by_stand_in: self.placeholders_by_stand_in.clone(), pattern: pattern.syntax().clone(), 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, }), _ => {} } @@ -171,15 +165,6 @@ impl RawPattern { } } -impl ParsedRule { - pub(crate) fn get_placeholder(&self, token: &SyntaxToken) -> Option<&Placeholder> { - if token.kind() != SyntaxKind::IDENT { - return None; - } - self.placeholders_by_stand_in.get(token.text()) - } -} - impl FromStr for SsrPattern { type Err = SsrError; diff --git a/crates/ra_ssr/src/replacing.rs b/crates/ra_ssr/src/replacing.rs index f1c5bdf14e..4b3f5509c3 100644 --- a/crates/ra_ssr/src/replacing.rs +++ b/crates/ra_ssr/src/replacing.rs @@ -1,9 +1,9 @@ //! Code for applying replacement templates for matches that have previously been found. use crate::matching::Var; -use crate::{parsing::ParsedRule, Match, SsrMatches}; -use ra_syntax::ast::AstToken; -use ra_syntax::{SyntaxElement, SyntaxNode, SyntaxToken, TextSize}; +use crate::{resolving::ResolvedRule, Match, SsrMatches}; +use ra_syntax::ast::{self, AstToken}; +use ra_syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextSize}; use ra_text_edit::TextEdit; /// Returns a text edit that will replace each match in `matches` with its corresponding replacement @@ -12,7 +12,7 @@ use ra_text_edit::TextEdit; pub(crate) fn matches_to_edit( matches: &SsrMatches, file_src: &str, - rules: &[ParsedRule], + rules: &[ResolvedRule], ) -> TextEdit { matches_to_edit_at_offset(matches, file_src, 0.into(), rules) } @@ -21,7 +21,7 @@ fn matches_to_edit_at_offset( matches: &SsrMatches, file_src: &str, relative_start: TextSize, - rules: &[ParsedRule], + rules: &[ResolvedRule], ) -> TextEdit { let mut edit_builder = ra_text_edit::TextEditBuilder::default(); for m in &matches.matches { @@ -36,11 +36,11 @@ fn matches_to_edit_at_offset( struct ReplacementRenderer<'a> { match_info: &'a Match, file_src: &'a str, - rules: &'a [ParsedRule], - rule: &'a ParsedRule, + rules: &'a [ResolvedRule], + rule: &'a ResolvedRule, } -fn render_replace(match_info: &Match, file_src: &str, rules: &[ParsedRule]) -> String { +fn render_replace(match_info: &Match, file_src: &str, rules: &[ResolvedRule]) -> String { let mut out = String::new(); let rule = &rules[match_info.rule_index]; let template = rule @@ -48,7 +48,7 @@ fn render_replace(match_info: &Match, file_src: &str, rules: &[ParsedRule]) -> S .as_ref() .expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern"); let renderer = ReplacementRenderer { match_info, file_src, rules, rule }; - renderer.render_node_children(&template, &mut out); + renderer.render_node(&template.node, &mut out); for comment in &match_info.ignored_comments { out.push_str(&comment.syntax().to_string()); } @@ -68,11 +68,31 @@ impl ReplacementRenderer<'_> { self.render_token(&token, out); } SyntaxElement::Node(child_node) => { - self.render_node_children(&child_node, out); + self.render_node(&child_node, out); } } } + fn render_node(&self, node: &SyntaxNode, out: &mut String) { + use ra_syntax::ast::AstNode; + if let Some(mod_path) = self.match_info.rendered_template_paths.get(&node) { + out.push_str(&mod_path.to_string()); + // Emit everything except for the segment's name-ref, since we already effectively + // emitted that as part of `mod_path`. + if let Some(path) = ast::Path::cast(node.clone()) { + if let Some(segment) = path.segment() { + for node_or_token in segment.syntax().children_with_tokens() { + if node_or_token.kind() != SyntaxKind::NAME_REF { + self.render_node_or_token(&node_or_token, out); + } + } + } + } + } else { + self.render_node_children(&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) = diff --git a/crates/ra_ssr/src/resolving.rs b/crates/ra_ssr/src/resolving.rs new file mode 100644 index 0000000000..e9d0521117 --- /dev/null +++ b/crates/ra_ssr/src/resolving.rs @@ -0,0 +1,153 @@ +//! This module is responsible for resolving paths within rules. + +use crate::errors::error; +use crate::{parsing, SsrError}; +use parsing::Placeholder; +use ra_syntax::{ast, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken}; +use rustc_hash::{FxHashMap, FxHashSet}; +use test_utils::mark; + +pub(crate) struct ResolvedRule { + pub(crate) pattern: ResolvedPattern, + pub(crate) template: Option, + pub(crate) index: usize, +} + +pub(crate) struct ResolvedPattern { + pub(crate) placeholders_by_stand_in: FxHashMap, + pub(crate) node: SyntaxNode, + // Paths in `node` that we've resolved. + pub(crate) resolved_paths: FxHashMap, +} + +pub(crate) struct ResolvedPath { + pub(crate) resolution: hir::PathResolution, +} + +impl ResolvedRule { + pub(crate) fn new( + rule: parsing::ParsedRule, + scope: &hir::SemanticsScope, + hygiene: &hir::Hygiene, + index: usize, + ) -> Result { + let resolver = + Resolver { scope, hygiene, placeholders_by_stand_in: rule.placeholders_by_stand_in }; + let resolved_template = if let Some(template) = rule.template { + Some(resolver.resolve_pattern_tree(template)?) + } else { + None + }; + Ok(ResolvedRule { + pattern: resolver.resolve_pattern_tree(rule.pattern)?, + template: resolved_template, + index, + }) + } + + pub(crate) fn get_placeholder(&self, token: &SyntaxToken) -> Option<&Placeholder> { + if token.kind() != SyntaxKind::IDENT { + return None; + } + self.pattern.placeholders_by_stand_in.get(token.text()) + } +} + +struct Resolver<'a, 'db> { + scope: &'a hir::SemanticsScope<'db>, + hygiene: &'a hir::Hygiene, + placeholders_by_stand_in: FxHashMap, +} + +impl Resolver<'_, '_> { + fn resolve_pattern_tree(&self, pattern: SyntaxNode) -> Result { + let mut resolved_paths = FxHashMap::default(); + self.resolve(pattern.clone(), &mut resolved_paths)?; + Ok(ResolvedPattern { + node: pattern, + resolved_paths, + placeholders_by_stand_in: self.placeholders_by_stand_in.clone(), + }) + } + + fn resolve( + &self, + node: SyntaxNode, + resolved_paths: &mut FxHashMap, + ) -> Result<(), SsrError> { + use ra_syntax::ast::AstNode; + if let Some(path) = ast::Path::cast(node.clone()) { + // Check if this is an appropriate place in the path to resolve. If the path is + // something like `a::B::::c` then we want to resolve `a::B`. If the path contains + // a placeholder. e.g. `a::$b::c` then we want to resolve `a`. + if !path_contains_type_arguments(path.qualifier()) + && !self.path_contains_placeholder(&path) + { + let resolution = self + .resolve_path(&path) + .ok_or_else(|| error!("Failed to resolve path `{}`", node.text()))?; + resolved_paths.insert(node, ResolvedPath { resolution }); + return Ok(()); + } + } + for node in node.children() { + self.resolve(node, resolved_paths)?; + } + Ok(()) + } + + /// Returns whether `path` contains a placeholder, but ignores any placeholders within type + /// arguments. + fn path_contains_placeholder(&self, path: &ast::Path) -> bool { + if let Some(segment) = path.segment() { + if let Some(name_ref) = segment.name_ref() { + if self.placeholders_by_stand_in.contains_key(name_ref.text()) { + return true; + } + } + } + if let Some(qualifier) = path.qualifier() { + return self.path_contains_placeholder(&qualifier); + } + false + } + + fn resolve_path(&self, path: &ast::Path) -> Option { + let hir_path = hir::Path::from_src(path.clone(), self.hygiene)?; + // First try resolving the whole path. This will work for things like + // `std::collections::HashMap`, but will fail for things like + // `std::collections::HashMap::new`. + if let Some(resolution) = self.scope.resolve_hir_path(&hir_path) { + return Some(resolution); + } + // Resolution failed, try resolving the qualifier (e.g. `std::collections::HashMap` and if + // that succeeds, then iterate through the candidates on the resolved type with the provided + // name. + let resolved_qualifier = self.scope.resolve_hir_path_qualifier(&hir_path.qualifier()?)?; + if let hir::PathResolution::Def(hir::ModuleDef::Adt(adt)) = resolved_qualifier { + adt.ty(self.scope.db).iterate_path_candidates( + self.scope.db, + self.scope.module()?.krate(), + &FxHashSet::default(), + Some(hir_path.segments().last()?.name), + |_ty, assoc_item| Some(hir::PathResolution::AssocItem(assoc_item)), + ) + } else { + None + } + } +} + +/// Returns whether `path` or any of its qualifiers contains type arguments. +fn path_contains_type_arguments(path: Option) -> bool { + if let Some(path) = path { + if let Some(segment) = path.segment() { + if segment.type_arg_list().is_some() { + mark::hit!(type_arguments_within_path); + return true; + } + } + return path_contains_type_arguments(path.qualifier()); + } + false +} diff --git a/crates/ra_ssr/src/search.rs b/crates/ra_ssr/src/search.rs index a28e9f3414..ccc2d544ab 100644 --- a/crates/ra_ssr/src/search.rs +++ b/crates/ra_ssr/src/search.rs @@ -1,6 +1,6 @@ //! Searching for matches. -use crate::{matching, parsing::ParsedRule, Match, MatchFinder}; +use crate::{matching, resolving::ResolvedRule, Match, MatchFinder}; use ra_db::FileRange; use ra_syntax::{ast, AstNode, SyntaxNode}; @@ -8,13 +8,13 @@ impl<'db> MatchFinder<'db> { /// Adds all matches for `rule` to `matches_out`. Matches may overlap in ways that make /// replacement impossible, so further processing is required in order to properly nest matches /// and remove overlapping matches. This is done in the `nesting` module. - pub(crate) fn find_matches_for_rule(&self, rule: &ParsedRule, matches_out: &mut Vec) { + pub(crate) fn find_matches_for_rule(&self, rule: &ResolvedRule, matches_out: &mut Vec) { // FIXME: Use resolved paths in the pattern to find places to search instead of always // scanning every node. self.slow_scan(rule, matches_out); } - fn slow_scan(&self, rule: &ParsedRule, matches_out: &mut Vec) { + fn slow_scan(&self, rule: &ResolvedRule, matches_out: &mut Vec) { use ra_db::SourceDatabaseExt; use ra_ide_db::symbol_index::SymbolsDatabase; for &root in self.sema.db.local_roots().iter() { @@ -30,7 +30,7 @@ impl<'db> MatchFinder<'db> { fn slow_scan_node( &self, code: &SyntaxNode, - rule: &ParsedRule, + rule: &ResolvedRule, restrict_range: &Option, matches_out: &mut Vec, ) { diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs index 63d5278947..33742dc8e1 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) { let mut match_finder = MatchFinder::in_context(&db, position); for rule in rules { let rule: SsrRule = rule.parse().unwrap(); - match_finder.add_rule(rule); + match_finder.add_rule(rule).unwrap(); } let edits = match_finder.edits(); if edits.is_empty() { @@ -114,7 +114,7 @@ fn print_match_debug_info(match_finder: &MatchFinder, file_id: FileId, snippet: fn assert_matches(pattern: &str, code: &str, expected: &[&str]) { let (db, position) = single_file(code); let mut match_finder = MatchFinder::in_context(&db, position); - match_finder.add_search_pattern(pattern.parse().unwrap()); + match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap(); let matched_strings: Vec = match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect(); if matched_strings != expected && !expected.is_empty() { @@ -126,7 +126,7 @@ fn assert_matches(pattern: &str, code: &str, expected: &[&str]) { fn assert_no_match(pattern: &str, code: &str) { let (db, position) = single_file(code); let mut match_finder = MatchFinder::in_context(&db, position); - match_finder.add_search_pattern(pattern.parse().unwrap()); + match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap(); let matches = match_finder.matches().flattened().matches; if !matches.is_empty() { 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) { fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) { let (db, position) = single_file(code); let mut match_finder = MatchFinder::in_context(&db, position); - match_finder.add_search_pattern(pattern.parse().unwrap()); + match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap(); let mut reasons = Vec::new(); for d in match_finder.debug_where_text_equal(position.file_id, snippet) { if let Some(reason) = d.match_failure_reason() { @@ -350,6 +350,60 @@ fn match_pattern() { assert_matches("Some($a)", "struct Some(); fn f() {if let Some(x) = foo() {}}", &["Some(x)"]); } +// If our pattern has a full path, e.g. a::b::c() and the code has c(), but c resolves to +// a::b::c, then we should match. +#[test] +fn match_fully_qualified_fn_path() { + let code = r#" + mod a { + pub mod b { + pub fn c(_: i32) {} + } + } + use a::b::c; + fn f1() { + c(42); + } + "#; + assert_matches("a::b::c($a)", code, &["c(42)"]); +} + +#[test] +fn match_resolved_type_name() { + let code = r#" + mod m1 { + pub mod m2 { + pub trait Foo {} + } + } + mod m3 { + trait Foo {} + fn f1(f: Option<&dyn Foo>) {} + } + mod m4 { + use crate::m1::m2::Foo; + fn f1(f: Option<&dyn Foo>) {} + } + "#; + assert_matches("m1::m2::Foo<$t>", code, &["Foo"]); +} + +#[test] +fn type_arguments_within_path() { + mark::check!(type_arguments_within_path); + let code = r#" + mod foo { + pub struct Bar {t: T} + impl Bar { + pub fn baz() {} + } + } + fn f1() {foo::Bar::::baz();} + "#; + assert_no_match("foo::Bar::::baz()", code); + assert_matches("foo::Bar::::baz()", code, &["foo::Bar::::baz()"]); +} + #[test] fn literal_constraint() { mark::check!(literal_constraint); @@ -482,6 +536,86 @@ fn replace_associated_function_call() { ); } +#[test] +fn replace_path_in_different_contexts() { + // Note the <|> inside module a::b which marks the point where the rule is interpreted. We + // replace foo with bar, but both need different path qualifiers in different contexts. In f4, + // foo is unqualified because of a use statement, however the replacement needs to be fully + // qualified. + assert_ssr_transform( + "c::foo() ==>> c::bar()", + r#" + mod a { + pub mod b {<|> + pub mod c { + pub fn foo() {} + pub fn bar() {} + fn f1() { foo() } + } + fn f2() { c::foo() } + } + fn f3() { b::c::foo() } + } + use a::b::c::foo; + fn f4() { foo() } + "#, + expect![[r#" + mod a { + pub mod b { + pub mod c { + pub fn foo() {} + pub fn bar() {} + fn f1() { bar() } + } + fn f2() { c::bar() } + } + fn f3() { b::c::bar() } + } + use a::b::c::foo; + fn f4() { a::b::c::bar() } + "#]], + ); +} + +#[test] +fn replace_associated_function_with_generics() { + assert_ssr_transform( + "c::Foo::<$a>::new() ==>> d::Bar::<$a>::default()", + r#" + mod c { + pub struct Foo {v: T} + impl Foo { pub fn new() {} } + fn f1() { + Foo::::new(); + } + } + mod d { + pub struct Bar {v: T} + impl Bar { pub fn default() {} } + fn f1() { + super::c::Foo::::new(); + } + } + "#, + expect![[r#" + mod c { + pub struct Foo {v: T} + impl Foo { pub fn new() {} } + fn f1() { + crate::d::Bar::::default(); + } + } + mod d { + pub struct Bar {v: T} + impl Bar { pub fn default() {} } + fn f1() { + Bar::::default(); + } + } + "#]], + ); +} + #[test] fn replace_type() { assert_ssr_transform( diff --git a/crates/rust-analyzer/src/cli/ssr.rs b/crates/rust-analyzer/src/cli/ssr.rs index 22f5b4be04..194bec008d 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) -> Result<()> { let db = host.raw_database(); let mut match_finder = MatchFinder::at_first_file(db)?; for rule in rules { - match_finder.add_rule(rule); + match_finder.add_rule(rule)?; } let edits = match_finder.edits(); for edit in edits { @@ -32,7 +32,7 @@ pub fn search_for_patterns(patterns: Vec, debug_snippet: Option