Skip to content

Commit 9042009

Browse files
Merge #5567
5567: SSR: Wrap placeholder expansions in parenthesis when necessary r=matklad a=davidlattimore e.g. `foo($a) ==> $a.to_string()` should produce `(1 + 2).to_string()` not `1 + 2.to_string()` We don't yet try to determine if the whole replacement needs to be wrapped in parenthesis. That's harder and I think perhaps less often an issue. Co-authored-by: David Lattimore <[email protected]>
2 parents be803ef + fa1e411 commit 9042009

File tree

2 files changed

+111
-20
lines changed

2 files changed

+111
-20
lines changed

crates/ra_ssr/src/replacing.rs

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
use crate::matching::Var;
44
use crate::{resolving::ResolvedRule, Match, SsrMatches};
55
use ra_syntax::ast::{self, AstToken};
6-
use ra_syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextSize};
6+
use ra_syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize};
77
use ra_text_edit::TextEdit;
8+
use rustc_hash::{FxHashMap, FxHashSet};
89

910
/// Returns a text edit that will replace each match in `matches` with its corresponding replacement
1011
/// template. Placeholders in the template will have been substituted with whatever they matched to
@@ -38,62 +39,79 @@ struct ReplacementRenderer<'a> {
3839
file_src: &'a str,
3940
rules: &'a [ResolvedRule],
4041
rule: &'a ResolvedRule,
42+
out: String,
43+
// Map from a range within `out` to a token in `template` that represents a placeholder. This is
44+
// used to validate that the generated source code doesn't split any placeholder expansions (see
45+
// below).
46+
placeholder_tokens_by_range: FxHashMap<TextRange, SyntaxToken>,
47+
// Which placeholder tokens need to be wrapped in parenthesis in order to ensure that when `out`
48+
// is parsed, placeholders don't get split. e.g. if a template of `$a.to_string()` results in `1
49+
// + 2.to_string()` then the placeholder value `1 + 2` was split and needs parenthesis.
50+
placeholder_tokens_requiring_parenthesis: FxHashSet<SyntaxToken>,
4151
}
4252

4353
fn render_replace(match_info: &Match, file_src: &str, rules: &[ResolvedRule]) -> String {
44-
let mut out = String::new();
4554
let rule = &rules[match_info.rule_index];
4655
let template = rule
4756
.template
4857
.as_ref()
4958
.expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern");
50-
let renderer = ReplacementRenderer { match_info, file_src, rules, rule };
51-
renderer.render_node(&template.node, &mut out);
59+
let mut renderer = ReplacementRenderer {
60+
match_info,
61+
file_src,
62+
rules,
63+
rule,
64+
out: String::new(),
65+
placeholder_tokens_requiring_parenthesis: FxHashSet::default(),
66+
placeholder_tokens_by_range: FxHashMap::default(),
67+
};
68+
renderer.render_node(&template.node);
69+
renderer.maybe_rerender_with_extra_parenthesis(&template.node);
5270
for comment in &match_info.ignored_comments {
53-
out.push_str(&comment.syntax().to_string());
71+
renderer.out.push_str(&comment.syntax().to_string());
5472
}
55-
out
73+
renderer.out
5674
}
5775

5876
impl ReplacementRenderer<'_> {
59-
fn render_node_children(&self, node: &SyntaxNode, out: &mut String) {
77+
fn render_node_children(&mut self, node: &SyntaxNode) {
6078
for node_or_token in node.children_with_tokens() {
61-
self.render_node_or_token(&node_or_token, out);
79+
self.render_node_or_token(&node_or_token);
6280
}
6381
}
6482

65-
fn render_node_or_token(&self, node_or_token: &SyntaxElement, out: &mut String) {
83+
fn render_node_or_token(&mut self, node_or_token: &SyntaxElement) {
6684
match node_or_token {
6785
SyntaxElement::Token(token) => {
68-
self.render_token(&token, out);
86+
self.render_token(&token);
6987
}
7088
SyntaxElement::Node(child_node) => {
71-
self.render_node(&child_node, out);
89+
self.render_node(&child_node);
7290
}
7391
}
7492
}
7593

76-
fn render_node(&self, node: &SyntaxNode, out: &mut String) {
94+
fn render_node(&mut self, node: &SyntaxNode) {
7795
use ra_syntax::ast::AstNode;
7896
if let Some(mod_path) = self.match_info.rendered_template_paths.get(&node) {
79-
out.push_str(&mod_path.to_string());
97+
self.out.push_str(&mod_path.to_string());
8098
// Emit everything except for the segment's name-ref, since we already effectively
8199
// emitted that as part of `mod_path`.
82100
if let Some(path) = ast::Path::cast(node.clone()) {
83101
if let Some(segment) = path.segment() {
84102
for node_or_token in segment.syntax().children_with_tokens() {
85103
if node_or_token.kind() != SyntaxKind::NAME_REF {
86-
self.render_node_or_token(&node_or_token, out);
104+
self.render_node_or_token(&node_or_token);
87105
}
88106
}
89107
}
90108
}
91109
} else {
92-
self.render_node_children(&node, out);
110+
self.render_node_children(&node);
93111
}
94112
}
95113

96-
fn render_token(&self, token: &SyntaxToken, out: &mut String) {
114+
fn render_token(&mut self, token: &SyntaxToken) {
97115
if let Some(placeholder) = self.rule.get_placeholder(&token) {
98116
if let Some(placeholder_value) =
99117
self.match_info.placeholder_values.get(&Var(placeholder.ident.to_string()))
@@ -107,8 +125,23 @@ impl ReplacementRenderer<'_> {
107125
range.start(),
108126
self.rules,
109127
);
128+
let needs_parenthesis =
129+
self.placeholder_tokens_requiring_parenthesis.contains(token);
110130
edit.apply(&mut matched_text);
111-
out.push_str(&matched_text);
131+
if needs_parenthesis {
132+
self.out.push('(');
133+
}
134+
self.placeholder_tokens_by_range.insert(
135+
TextRange::new(
136+
TextSize::of(&self.out),
137+
TextSize::of(&self.out) + TextSize::of(&matched_text),
138+
),
139+
token.clone(),
140+
);
141+
self.out.push_str(&matched_text);
142+
if needs_parenthesis {
143+
self.out.push(')');
144+
}
112145
} else {
113146
// We validated that all placeholder references were valid before we
114147
// started, so this shouldn't happen.
@@ -118,7 +151,44 @@ impl ReplacementRenderer<'_> {
118151
);
119152
}
120153
} else {
121-
out.push_str(token.text().as_str());
154+
self.out.push_str(token.text().as_str());
155+
}
156+
}
157+
158+
// Checks if the resulting code, when parsed doesn't split any placeholders due to different
159+
// order of operations between the search pattern and the replacement template. If any do, then
160+
// we rerender the template and wrap the problematic placeholders with parenthesis.
161+
fn maybe_rerender_with_extra_parenthesis(&mut self, template: &SyntaxNode) {
162+
if let Some(node) = parse_as_kind(&self.out, template.kind()) {
163+
self.remove_node_ranges(node);
164+
if self.placeholder_tokens_by_range.is_empty() {
165+
return;
166+
}
167+
self.placeholder_tokens_requiring_parenthesis =
168+
self.placeholder_tokens_by_range.values().cloned().collect();
169+
self.out.clear();
170+
self.render_node(template);
171+
}
172+
}
173+
174+
fn remove_node_ranges(&mut self, node: SyntaxNode) {
175+
self.placeholder_tokens_by_range.remove(&node.text_range());
176+
for child in node.children() {
177+
self.remove_node_ranges(child);
178+
}
179+
}
180+
}
181+
182+
fn parse_as_kind(code: &str, kind: SyntaxKind) -> Option<SyntaxNode> {
183+
use ra_syntax::ast::AstNode;
184+
if ast::Expr::can_cast(kind) {
185+
if let Ok(expr) = ast::Expr::parse(code) {
186+
return Some(expr.syntax().clone());
187+
}
188+
} else if ast::Item::can_cast(kind) {
189+
if let Ok(item) = ast::Item::parse(code) {
190+
return Some(item.syntax().clone());
122191
}
123192
}
193+
None
124194
}

crates/ra_ssr/src/tests.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ fn replace_binary_op() {
664664
assert_ssr_transform(
665665
"$a + $b ==>> $b + $a",
666666
"fn f() {1 + 2 + 3 + 4}",
667-
expect![["fn f() {4 + 3 + 2 + 1}"]],
667+
expect![[r#"fn f() {4 + (3 + (2 + 1))}"#]],
668668
);
669669
}
670670

@@ -773,11 +773,32 @@ fn preserves_whitespace_within_macro_expansion() {
773773
macro_rules! macro1 {
774774
($a:expr) => {$a}
775775
}
776-
fn f() {macro1!(4 - 3 - 1 * 2}
776+
fn f() {macro1!(4 - (3 - 1 * 2)}
777777
"#]],
778778
)
779779
}
780780

781+
#[test]
782+
fn add_parenthesis_when_necessary() {
783+
assert_ssr_transform(
784+
"foo($a) ==>> $a.to_string()",
785+
r#"
786+
fn foo(_: i32) {}
787+
fn bar3(v: i32) {
788+
foo(1 + 2);
789+
foo(-v);
790+
}
791+
"#,
792+
expect![[r#"
793+
fn foo(_: i32) {}
794+
fn bar3(v: i32) {
795+
(1 + 2).to_string();
796+
(-v).to_string();
797+
}
798+
"#]],
799+
)
800+
}
801+
781802
#[test]
782803
fn match_failure_reasons() {
783804
let code = r#"

0 commit comments

Comments
 (0)