Skip to content

Commit 04d2b7b

Browse files
Merge #5565
5565: SSR: Don't mix non-path-based rules with path-based r=matklad a=davidlattimore If any rules contain paths, then we reject any rules that don't contain paths. Allowing a mix leads to strange semantics, since the path-based rules only match things where the path refers to semantically the same thing, whereas the non-path-based rules could match anything. Specifically, if we have a rule like `foo ==>> bar` we only want to match the `foo` that is in the current scope, not any `foo`. However "foo" can be parsed as a pattern (BIND_PAT -> NAME -> IDENT). Allowing such a rule through would result in renaming everything called `foo` to `bar`. It'd also be slow, since without a path, we'd have to use the slow-scan search mechanism. Co-authored-by: David Lattimore <[email protected]>
2 parents 82e390f + 3600c43 commit 04d2b7b

File tree

2 files changed

+62
-1
lines changed

2 files changed

+62
-1
lines changed

crates/ra_ssr/src/parsing.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{SsrError, SsrPattern, SsrRule};
1010
use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, T};
1111
use rustc_hash::{FxHashMap, FxHashSet};
1212
use std::str::FromStr;
13+
use test_utils::mark;
1314

1415
#[derive(Debug)]
1516
pub(crate) struct ParsedRule {
@@ -102,14 +103,35 @@ impl RuleBuilder {
102103
}
103104
}
104105

105-
fn build(self) -> Result<Vec<ParsedRule>, SsrError> {
106+
fn build(mut self) -> Result<Vec<ParsedRule>, SsrError> {
106107
if self.rules.is_empty() {
107108
bail!("Not a valid Rust expression, type, item, path or pattern");
108109
}
110+
// If any rules contain paths, then we reject any rules that don't contain paths. Allowing a
111+
// mix leads to strange semantics, since the path-based rules only match things where the
112+
// path refers to semantically the same thing, whereas the non-path-based rules could match
113+
// anything. Specifically, if we have a rule like `foo ==>> bar` we only want to match the
114+
// `foo` that is in the current scope, not any `foo`. However "foo" can be parsed as a
115+
// pattern (BIND_PAT -> NAME -> IDENT). Allowing such a rule through would result in
116+
// renaming everything called `foo` to `bar`. It'd also be slow, since without a path, we'd
117+
// have to use the slow-scan search mechanism.
118+
if self.rules.iter().any(|rule| contains_path(&rule.pattern)) {
119+
let old_len = self.rules.len();
120+
self.rules.retain(|rule| contains_path(&rule.pattern));
121+
if self.rules.len() < old_len {
122+
mark::hit!(pattern_is_a_single_segment_path);
123+
}
124+
}
109125
Ok(self.rules)
110126
}
111127
}
112128

129+
/// Returns whether there are any paths in `node`.
130+
fn contains_path(node: &SyntaxNode) -> bool {
131+
node.kind() == SyntaxKind::PATH
132+
|| node.descendants().any(|node| node.kind() == SyntaxKind::PATH)
133+
}
134+
113135
impl FromStr for SsrRule {
114136
type Err = SsrError;
115137

crates/ra_ssr/src/tests.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,45 @@ fn ufcs_matches_method_call() {
886886
);
887887
}
888888

889+
#[test]
890+
fn pattern_is_a_single_segment_path() {
891+
mark::check!(pattern_is_a_single_segment_path);
892+
// The first function should not be altered because the `foo` in scope at the cursor position is
893+
// a different `foo`. This case is special because "foo" can be parsed as a pattern (BIND_PAT ->
894+
// NAME -> IDENT), which contains no path. If we're not careful we'll end up matching the `foo`
895+
// in `let foo` from the first function. Whether we should match the `let foo` in the second
896+
// function is less clear. At the moment, we don't. Doing so sounds like a rename operation,
897+
// which isn't really what SSR is for, especially since the replacement `bar` must be able to be
898+
// resolved, which means if we rename `foo` we'll get a name collision.
899+
assert_ssr_transform(
900+
"foo ==>> bar",
901+
r#"
902+
fn f1() -> i32 {
903+
let foo = 1;
904+
let bar = 2;
905+
foo
906+
}
907+
fn f1() -> i32 {
908+
let foo = 1;
909+
let bar = 2;
910+
foo<|>
911+
}
912+
"#,
913+
expect![[r#"
914+
fn f1() -> i32 {
915+
let foo = 1;
916+
let bar = 2;
917+
foo
918+
}
919+
fn f1() -> i32 {
920+
let foo = 1;
921+
let bar = 2;
922+
bar
923+
}
924+
"#]],
925+
);
926+
}
927+
889928
#[test]
890929
fn replace_local_variable_reference() {
891930
// The pattern references a local variable `foo` in the block containing the cursor. We should

0 commit comments

Comments
 (0)