SSR: Allow matching of whole macro calls
Matching within macro calls is to come later and matching of macro calls within macro calls later still.
This commit is contained in:
parent
d8842e89e9
commit
467af611fb
@ -9,6 +9,7 @@
|
||||
// Search and replace with named wildcards that will match any expression, type, path, pattern or item.
|
||||
// The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`.
|
||||
// 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.
|
||||
// Available via the command `rust-analyzer.ssr`.
|
||||
//
|
||||
// ```rust
|
||||
|
@ -91,14 +91,16 @@ fn find_matches(
|
||||
if let Ok(mut m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) {
|
||||
// Continue searching in each of our placeholders.
|
||||
for placeholder_value in m.placeholder_values.values_mut() {
|
||||
// Don't search our placeholder if it's the entire matched node, otherwise we'd
|
||||
// find the same match over and over until we got a stack overflow.
|
||||
if placeholder_value.node != *code {
|
||||
self.find_matches(
|
||||
&placeholder_value.node,
|
||||
restrict_range,
|
||||
&mut placeholder_value.inner_matches,
|
||||
);
|
||||
if let Some(placeholder_node) = &placeholder_value.node {
|
||||
// Don't search our placeholder if it's the entire matched node, otherwise we'd
|
||||
// find the same match over and over until we got a stack overflow.
|
||||
if placeholder_node != code {
|
||||
self.find_matches(
|
||||
placeholder_node,
|
||||
restrict_range,
|
||||
&mut placeholder_value.inner_matches,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
matches_out.matches.push(m);
|
||||
|
@ -61,8 +61,9 @@ pub(crate) struct Match {
|
||||
/// Information about a placeholder bound in a match.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PlaceholderMatch {
|
||||
/// The node that the placeholder matched to.
|
||||
pub(crate) node: SyntaxNode,
|
||||
/// The node that the placeholder matched to. If set, then we'll search for further matches
|
||||
/// within this node. It isn't set when we match tokens within a macro call's token tree.
|
||||
pub(crate) node: Option<SyntaxNode>,
|
||||
pub(crate) range: FileRange,
|
||||
/// More matches, found within `node`.
|
||||
pub(crate) inner_matches: SsrMatches,
|
||||
@ -195,6 +196,7 @@ fn attempt_match_node(
|
||||
SyntaxKind::RECORD_FIELD_LIST => {
|
||||
self.attempt_match_record_field_list(match_inputs, pattern, code)
|
||||
}
|
||||
SyntaxKind::TOKEN_TREE => self.attempt_match_token_tree(match_inputs, pattern, code),
|
||||
_ => self.attempt_match_node_children(match_inputs, pattern, code),
|
||||
}
|
||||
}
|
||||
@ -340,6 +342,90 @@ fn attempt_match_record_field_list(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Outside of token trees, a placeholder can only match a single AST node, whereas in a token
|
||||
/// tree it can match a sequence of tokens.
|
||||
fn attempt_match_token_tree(
|
||||
&mut self,
|
||||
match_inputs: &MatchInputs,
|
||||
pattern: &SyntaxNode,
|
||||
code: &ra_syntax::SyntaxNode,
|
||||
) -> Result<(), MatchFailed> {
|
||||
let mut pattern = PatternIterator::new(pattern).peekable();
|
||||
let mut children = code.children_with_tokens();
|
||||
while let Some(child) = children.next() {
|
||||
if let Some(placeholder) = pattern.peek().and_then(|p| match_inputs.get_placeholder(p))
|
||||
{
|
||||
pattern.next();
|
||||
let next_pattern_token = pattern
|
||||
.peek()
|
||||
.and_then(|p| match p {
|
||||
SyntaxElement::Token(t) => Some(t.clone()),
|
||||
SyntaxElement::Node(n) => n.first_token(),
|
||||
})
|
||||
.map(|p| p.text().to_string());
|
||||
let first_matched_token = child.clone();
|
||||
let mut last_matched_token = child;
|
||||
// Read code tokens util we reach one equal to the next token from our pattern
|
||||
// or we reach the end of the token tree.
|
||||
while let Some(next) = children.next() {
|
||||
match &next {
|
||||
SyntaxElement::Token(t) => {
|
||||
if Some(t.to_string()) == next_pattern_token {
|
||||
pattern.next();
|
||||
break;
|
||||
}
|
||||
}
|
||||
SyntaxElement::Node(n) => {
|
||||
if let Some(first_token) = n.first_token() {
|
||||
if Some(first_token.to_string()) == next_pattern_token {
|
||||
if let Some(SyntaxElement::Node(p)) = pattern.next() {
|
||||
// We have a subtree that starts with the next token in our pattern.
|
||||
self.attempt_match_token_tree(match_inputs, &p, &n)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
last_matched_token = next;
|
||||
}
|
||||
if let Some(match_out) = &mut self.match_out {
|
||||
match_out.placeholder_values.insert(
|
||||
Var(placeholder.ident.to_string()),
|
||||
PlaceholderMatch::from_range(FileRange {
|
||||
file_id: self.sema.original_range(code).file_id,
|
||||
range: first_matched_token
|
||||
.text_range()
|
||||
.cover(last_matched_token.text_range()),
|
||||
}),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Match literal (non-placeholder) tokens.
|
||||
match child {
|
||||
SyntaxElement::Token(token) => {
|
||||
self.attempt_match_token(&mut pattern, &token)?;
|
||||
}
|
||||
SyntaxElement::Node(node) => match pattern.next() {
|
||||
Some(SyntaxElement::Node(p)) => {
|
||||
self.attempt_match_token_tree(match_inputs, &p, &node)?;
|
||||
}
|
||||
Some(SyntaxElement::Token(p)) => fail_match!(
|
||||
"Pattern has token '{}', code has subtree '{}'",
|
||||
p.text(),
|
||||
node.text()
|
||||
),
|
||||
None => fail_match!("Pattern has nothing, code has '{}'", node.text()),
|
||||
},
|
||||
}
|
||||
}
|
||||
if let Some(p) = pattern.next() {
|
||||
fail_match!("Reached end of token tree in code, but pattern still has {:?}", p);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_non_trivial(&mut self, code_it: &mut SyntaxElementChildren) -> Option<SyntaxElement> {
|
||||
loop {
|
||||
let c = code_it.next();
|
||||
@ -399,7 +485,11 @@ fn recording_match_fail_reasons() -> bool {
|
||||
|
||||
impl PlaceholderMatch {
|
||||
fn new(node: &SyntaxNode, range: FileRange) -> Self {
|
||||
Self { node: node.clone(), range, inner_matches: SsrMatches::default() }
|
||||
Self { node: Some(node.clone()), range, inner_matches: SsrMatches::default() }
|
||||
}
|
||||
|
||||
fn from_range(range: FileRange) -> Self {
|
||||
Self { node: None, range, inner_matches: SsrMatches::default() }
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,7 +574,14 @@ fn parse_match_replace() {
|
||||
assert_eq!(matches.matches.len(), 1);
|
||||
assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)");
|
||||
assert_eq!(matches.matches[0].placeholder_values.len(), 1);
|
||||
assert_eq!(matches.matches[0].placeholder_values[&Var("x".to_string())].node.text(), "1+2");
|
||||
assert_eq!(
|
||||
matches.matches[0].placeholder_values[&Var("x".to_string())]
|
||||
.node
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.text(),
|
||||
"1+2"
|
||||
);
|
||||
|
||||
let edit = crate::replacing::matches_to_edit(&matches);
|
||||
let mut after = input.to_string();
|
||||
|
@ -24,6 +24,7 @@ fn matches_to_edit_at_offset(matches: &SsrMatches, relative_start: TextSize) ->
|
||||
|
||||
fn render_replace(match_info: &Match) -> String {
|
||||
let mut out = String::new();
|
||||
let match_start = match_info.matched_node.text_range().start();
|
||||
for r in &match_info.template.tokens {
|
||||
match r {
|
||||
PatternElement::Token(t) => out.push_str(t.text.as_str()),
|
||||
@ -32,7 +33,14 @@ fn render_replace(match_info: &Match) -> String {
|
||||
match_info.placeholder_values.get(&Var(p.ident.to_string()))
|
||||
{
|
||||
let range = &placeholder_value.range.range;
|
||||
let mut matched_text = placeholder_value.node.text().to_string();
|
||||
let mut matched_text = if let Some(node) = &placeholder_value.node {
|
||||
node.text().to_string()
|
||||
} else {
|
||||
let relative_range = range.checked_sub(match_start).unwrap();
|
||||
match_info.matched_node.text().to_string()
|
||||
[usize::from(relative_range.start())..usize::from(relative_range.end())]
|
||||
.to_string()
|
||||
};
|
||||
let edit =
|
||||
matches_to_edit_at_offset(&placeholder_value.inner_matches, range.start());
|
||||
edit.apply(&mut matched_text);
|
||||
|
@ -426,6 +426,45 @@ fn match_reordered_struct_instantiation() {
|
||||
assert_no_match("Foo {a: 1, z: 9}", "fn f() {Foo {a: 1}}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_macro_invocation() {
|
||||
assert_matches("foo!($a)", "fn() {foo(foo!(foo()))}", &["foo!(foo())"]);
|
||||
assert_matches("foo!(41, $a, 43)", "fn() {foo!(41, 42, 43)}", &["foo!(41, 42, 43)"]);
|
||||
assert_no_match("foo!(50, $a, 43)", "fn() {foo!(41, 42, 43}");
|
||||
assert_no_match("foo!(41, $a, 50)", "fn() {foo!(41, 42, 43}");
|
||||
assert_matches("foo!($a())", "fn() {foo!(bar())}", &["foo!(bar())"]);
|
||||
}
|
||||
|
||||
// When matching within a macro expansion, we only allow matches of nodes that originated from
|
||||
// the macro call, not from the macro definition.
|
||||
#[test]
|
||||
fn no_match_expression_from_macro() {
|
||||
assert_no_match(
|
||||
"$a.clone()",
|
||||
r#"
|
||||
macro_rules! m1 {
|
||||
() => {42.clone()}
|
||||
}
|
||||
fn f1() {m1!()}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
// We definitely don't want to allow matching of an expression that part originates from the
|
||||
// macro call `42` and part from the macro definition `.clone()`.
|
||||
#[test]
|
||||
fn no_match_split_expression() {
|
||||
assert_no_match(
|
||||
"$a.clone()",
|
||||
r#"
|
||||
macro_rules! m1 {
|
||||
($x:expr) => {$x.clone()}
|
||||
}
|
||||
fn f1() {m1!(42)}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_function_call() {
|
||||
assert_ssr_transform("foo() ==>> bar()", "fn f1() {foo(); foo();}", "fn f1() {bar(); bar();}");
|
||||
@ -467,6 +506,20 @@ fn replace_struct_init() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_macro_invocations() {
|
||||
assert_ssr_transform(
|
||||
"try!($a) ==>> $a?",
|
||||
"fn f1() -> Result<(), E> {bar(try!(foo()));}",
|
||||
"fn f1() -> Result<(), E> {bar(foo()?);}",
|
||||
);
|
||||
assert_ssr_transform(
|
||||
"foo!($a($b)) ==>> foo($b, $a)",
|
||||
"fn f1() {foo!(abc(def() + 2));}",
|
||||
"fn f1() {foo(def() + 2, abc);}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_binary_op() {
|
||||
assert_ssr_transform(
|
||||
|
Loading…
Reference in New Issue
Block a user