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.
|
// 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>`.
|
// 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.
|
// 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`.
|
// Available via the command `rust-analyzer.ssr`.
|
||||||
//
|
//
|
||||||
// ```rust
|
// ```rust
|
||||||
|
@ -91,14 +91,16 @@ fn find_matches(
|
|||||||
if let Ok(mut m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) {
|
if let Ok(mut m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) {
|
||||||
// Continue searching in each of our placeholders.
|
// Continue searching in each of our placeholders.
|
||||||
for placeholder_value in m.placeholder_values.values_mut() {
|
for placeholder_value in m.placeholder_values.values_mut() {
|
||||||
// Don't search our placeholder if it's the entire matched node, otherwise we'd
|
if let Some(placeholder_node) = &placeholder_value.node {
|
||||||
// find the same match over and over until we got a stack overflow.
|
// Don't search our placeholder if it's the entire matched node, otherwise we'd
|
||||||
if placeholder_value.node != *code {
|
// find the same match over and over until we got a stack overflow.
|
||||||
self.find_matches(
|
if placeholder_node != code {
|
||||||
&placeholder_value.node,
|
self.find_matches(
|
||||||
restrict_range,
|
placeholder_node,
|
||||||
&mut placeholder_value.inner_matches,
|
restrict_range,
|
||||||
);
|
&mut placeholder_value.inner_matches,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
matches_out.matches.push(m);
|
matches_out.matches.push(m);
|
||||||
|
@ -61,8 +61,9 @@ pub(crate) struct Match {
|
|||||||
/// Information about a placeholder bound in a match.
|
/// Information about a placeholder bound in a match.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct PlaceholderMatch {
|
pub(crate) struct PlaceholderMatch {
|
||||||
/// The node that the placeholder matched to.
|
/// The node that the placeholder matched to. If set, then we'll search for further matches
|
||||||
pub(crate) node: SyntaxNode,
|
/// 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,
|
pub(crate) range: FileRange,
|
||||||
/// More matches, found within `node`.
|
/// More matches, found within `node`.
|
||||||
pub(crate) inner_matches: SsrMatches,
|
pub(crate) inner_matches: SsrMatches,
|
||||||
@ -195,6 +196,7 @@ fn attempt_match_node(
|
|||||||
SyntaxKind::RECORD_FIELD_LIST => {
|
SyntaxKind::RECORD_FIELD_LIST => {
|
||||||
self.attempt_match_record_field_list(match_inputs, pattern, code)
|
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),
|
_ => self.attempt_match_node_children(match_inputs, pattern, code),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -340,6 +342,90 @@ fn attempt_match_record_field_list(
|
|||||||
Ok(())
|
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> {
|
fn next_non_trivial(&mut self, code_it: &mut SyntaxElementChildren) -> Option<SyntaxElement> {
|
||||||
loop {
|
loop {
|
||||||
let c = code_it.next();
|
let c = code_it.next();
|
||||||
@ -399,7 +485,11 @@ fn recording_match_fail_reasons() -> bool {
|
|||||||
|
|
||||||
impl PlaceholderMatch {
|
impl PlaceholderMatch {
|
||||||
fn new(node: &SyntaxNode, range: FileRange) -> Self {
|
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.len(), 1);
|
||||||
assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)");
|
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.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 edit = crate::replacing::matches_to_edit(&matches);
|
||||||
let mut after = input.to_string();
|
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 {
|
fn render_replace(match_info: &Match) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
|
let match_start = match_info.matched_node.text_range().start();
|
||||||
for r in &match_info.template.tokens {
|
for r in &match_info.template.tokens {
|
||||||
match r {
|
match r {
|
||||||
PatternElement::Token(t) => out.push_str(t.text.as_str()),
|
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()))
|
match_info.placeholder_values.get(&Var(p.ident.to_string()))
|
||||||
{
|
{
|
||||||
let range = &placeholder_value.range.range;
|
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 =
|
let edit =
|
||||||
matches_to_edit_at_offset(&placeholder_value.inner_matches, range.start());
|
matches_to_edit_at_offset(&placeholder_value.inner_matches, range.start());
|
||||||
edit.apply(&mut matched_text);
|
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}}");
|
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]
|
#[test]
|
||||||
fn replace_function_call() {
|
fn replace_function_call() {
|
||||||
assert_ssr_transform("foo() ==>> bar()", "fn f1() {foo(); foo();}", "fn f1() {bar(); bar();}");
|
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]
|
#[test]
|
||||||
fn replace_binary_op() {
|
fn replace_binary_op() {
|
||||||
assert_ssr_transform(
|
assert_ssr_transform(
|
||||||
|
Loading…
Reference in New Issue
Block a user