mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +00:00
SSR: Match paths based on what they resolve to
Also render template paths appropriately for their context.
This commit is contained in:
parent
3975952601
commit
757f755c29
9 changed files with 482 additions and 61 deletions
|
@ -11,6 +11,16 @@ use ra_ssr::{MatchFinder, SsrError, SsrRule};
|
|||
// A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` 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 `${<name>:<constraint1>:<constraint2>...}`.
|
||||
//
|
||||
// Supported constraints:
|
||||
|
@ -47,7 +57,7 @@ pub fn parse_search_replace(
|
|||
) -> Result<Vec<SourceFileEdit>, 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());
|
||||
}
|
||||
|
|
|
@ -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<parsing::ParsedRule>,
|
||||
rules: Vec<ResolvedRule>,
|
||||
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<parsing::ParsedRule>) {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<SyntaxNode, hir::ModPath>,
|
||||
}
|
||||
|
||||
/// 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<FileRange>,
|
||||
sema: &Semantics<ra_ide_db::RootDatabase>,
|
||||
|
@ -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<FileRange>,
|
||||
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<FileRange>,
|
||||
sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>,
|
||||
) -> Result<Match, MatchFailed> {
|
||||
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<T: AstNode>(
|
||||
&self,
|
||||
phase: &mut Phase,
|
||||
pattern: Option<T>,
|
||||
code: Option<T>,
|
||||
) -> 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<ra_ide_db::RootDatabase>,
|
||||
) -> 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<SyntaxElement> {
|
||||
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)");
|
||||
|
|
|
@ -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<SmolStr, Placeholder>,
|
||||
pub(crate) pattern: SyntaxNode,
|
||||
pub(crate) template: Option<SyntaxNode>,
|
||||
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<usize>, 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;
|
||||
|
||||
|
|
|
@ -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) =
|
||||
|
|
153
crates/ra_ssr/src/resolving.rs
Normal file
153
crates/ra_ssr/src/resolving.rs
Normal file
|
@ -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<ResolvedPattern>,
|
||||
pub(crate) index: usize,
|
||||
}
|
||||
|
||||
pub(crate) struct ResolvedPattern {
|
||||
pub(crate) placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>,
|
||||
pub(crate) node: SyntaxNode,
|
||||
// Paths in `node` that we've resolved.
|
||||
pub(crate) resolved_paths: FxHashMap<SyntaxNode, ResolvedPath>,
|
||||
}
|
||||
|
||||
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<ResolvedRule, SsrError> {
|
||||
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<SmolStr, parsing::Placeholder>,
|
||||
}
|
||||
|
||||
impl Resolver<'_, '_> {
|
||||
fn resolve_pattern_tree(&self, pattern: SyntaxNode) -> Result<ResolvedPattern, SsrError> {
|
||||
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<SyntaxNode, ResolvedPath>,
|
||||
) -> 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::<i32>::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<hir::PathResolution> {
|
||||
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<ast::Path>) -> 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
|
||||
}
|
|
@ -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<Match>) {
|
||||
pub(crate) fn find_matches_for_rule(&self, rule: &ResolvedRule, matches_out: &mut Vec<Match>) {
|
||||
// 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<Match>) {
|
||||
fn slow_scan(&self, rule: &ResolvedRule, matches_out: &mut Vec<Match>) {
|
||||
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<FileRange>,
|
||||
matches_out: &mut Vec<Match>,
|
||||
) {
|
||||
|
|
|
@ -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<String> =
|
||||
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<T> {}
|
||||
}
|
||||
}
|
||||
mod m3 {
|
||||
trait Foo<T> {}
|
||||
fn f1(f: Option<&dyn Foo<bool>>) {}
|
||||
}
|
||||
mod m4 {
|
||||
use crate::m1::m2::Foo;
|
||||
fn f1(f: Option<&dyn Foo<i32>>) {}
|
||||
}
|
||||
"#;
|
||||
assert_matches("m1::m2::Foo<$t>", code, &["Foo<i32>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_arguments_within_path() {
|
||||
mark::check!(type_arguments_within_path);
|
||||
let code = r#"
|
||||
mod foo {
|
||||
pub struct Bar<T> {t: T}
|
||||
impl<T> Bar<T> {
|
||||
pub fn baz() {}
|
||||
}
|
||||
}
|
||||
fn f1() {foo::Bar::<i32>::baz();}
|
||||
"#;
|
||||
assert_no_match("foo::Bar::<i64>::baz()", code);
|
||||
assert_matches("foo::Bar::<i32>::baz()", code, &["foo::Bar::<i32>::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<T> {v: T}
|
||||
impl<T> Foo<T> { pub fn new() {} }
|
||||
fn f1() {
|
||||
Foo::<i32>::new();
|
||||
}
|
||||
}
|
||||
mod d {
|
||||
pub struct Bar<T> {v: T}
|
||||
impl<T> Bar<T> { pub fn default() {} }
|
||||
fn f1() {
|
||||
super::c::Foo::<i32>::new();
|
||||
}
|
||||
}
|
||||
"#,
|
||||
expect![[r#"
|
||||
mod c {
|
||||
pub struct Foo<T> {v: T}
|
||||
impl<T> Foo<T> { pub fn new() {} }
|
||||
fn f1() {
|
||||
crate::d::Bar::<i32>::default();
|
||||
}
|
||||
}
|
||||
mod d {
|
||||
pub struct Bar<T> {v: T}
|
||||
impl<T> Bar<T> { pub fn default() {} }
|
||||
fn f1() {
|
||||
Bar::<i32>::default();
|
||||
}
|
||||
}
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_type() {
|
||||
assert_ssr_transform(
|
||||
|
|
|
@ -9,7 +9,7 @@ pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> 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<SsrPattern>, debug_snippet: Option<Stri
|
|||
let db = host.raw_database();
|
||||
let mut match_finder = MatchFinder::at_first_file(db)?;
|
||||
for pattern in patterns {
|
||||
match_finder.add_search_pattern(pattern);
|
||||
match_finder.add_search_pattern(pattern)?;
|
||||
}
|
||||
if let Some(debug_snippet) = &debug_snippet {
|
||||
for &root in db.local_roots().iter() {
|
||||
|
|
Loading…
Reference in a new issue