diff options
Diffstat (limited to 'crates/ra_ssr')
-rw-r--r-- | crates/ra_ssr/src/lib.rs | 165 | ||||
-rw-r--r-- | crates/ra_ssr/src/matching.rs | 14 | ||||
-rw-r--r-- | crates/ra_ssr/src/parsing.rs | 4 | ||||
-rw-r--r-- | crates/ra_ssr/src/replacing.rs | 35 | ||||
-rw-r--r-- | crates/ra_ssr/src/tests.rs | 212 |
5 files changed, 248 insertions, 182 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs index 8f149e3db..422e15ee6 100644 --- a/crates/ra_ssr/src/lib.rs +++ b/crates/ra_ssr/src/lib.rs | |||
@@ -9,10 +9,11 @@ mod replacing; | |||
9 | #[cfg(test)] | 9 | #[cfg(test)] |
10 | mod tests; | 10 | mod tests; |
11 | 11 | ||
12 | use crate::matching::Match; | 12 | pub use crate::matching::Match; |
13 | use crate::matching::{record_match_fails_reasons_scope, MatchFailureReason}; | ||
13 | use hir::Semantics; | 14 | use hir::Semantics; |
14 | use ra_db::{FileId, FileRange}; | 15 | use ra_db::{FileId, FileRange}; |
15 | use ra_syntax::{ast, AstNode, SmolStr, SyntaxNode}; | 16 | use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, TextRange}; |
16 | use ra_text_edit::TextEdit; | 17 | use ra_text_edit::TextEdit; |
17 | use rustc_hash::FxHashMap; | 18 | use rustc_hash::FxHashMap; |
18 | 19 | ||
@@ -26,7 +27,7 @@ pub struct SsrRule { | |||
26 | } | 27 | } |
27 | 28 | ||
28 | #[derive(Debug)] | 29 | #[derive(Debug)] |
29 | struct SsrPattern { | 30 | pub struct SsrPattern { |
30 | raw: parsing::RawSearchPattern, | 31 | raw: parsing::RawSearchPattern, |
31 | /// Placeholders keyed by the stand-in ident that we use in Rust source code. | 32 | /// Placeholders keyed by the stand-in ident that we use in Rust source code. |
32 | placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>, | 33 | placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>, |
@@ -45,7 +46,7 @@ pub struct SsrError(String); | |||
45 | 46 | ||
46 | #[derive(Debug, Default)] | 47 | #[derive(Debug, Default)] |
47 | pub struct SsrMatches { | 48 | pub struct SsrMatches { |
48 | matches: Vec<Match>, | 49 | pub matches: Vec<Match>, |
49 | } | 50 | } |
50 | 51 | ||
51 | /// Searches a crate for pattern matches and possibly replaces them with something else. | 52 | /// Searches a crate for pattern matches and possibly replaces them with something else. |
@@ -64,16 +65,23 @@ impl<'db> MatchFinder<'db> { | |||
64 | self.rules.push(rule); | 65 | self.rules.push(rule); |
65 | } | 66 | } |
66 | 67 | ||
68 | /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you | ||
69 | /// intend to do replacement, use `add_rule` instead. | ||
70 | pub fn add_search_pattern(&mut self, pattern: SsrPattern) { | ||
71 | self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) | ||
72 | } | ||
73 | |||
67 | pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { | 74 | pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { |
68 | let matches = self.find_matches_in_file(file_id); | 75 | let matches = self.find_matches_in_file(file_id); |
69 | if matches.matches.is_empty() { | 76 | if matches.matches.is_empty() { |
70 | None | 77 | None |
71 | } else { | 78 | } else { |
72 | Some(replacing::matches_to_edit(&matches)) | 79 | use ra_db::SourceDatabaseExt; |
80 | Some(replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id))) | ||
73 | } | 81 | } |
74 | } | 82 | } |
75 | 83 | ||
76 | fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { | 84 | pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { |
77 | let file = self.sema.parse(file_id); | 85 | let file = self.sema.parse(file_id); |
78 | let code = file.syntax(); | 86 | let code = file.syntax(); |
79 | let mut matches = SsrMatches::default(); | 87 | let mut matches = SsrMatches::default(); |
@@ -81,6 +89,32 @@ impl<'db> MatchFinder<'db> { | |||
81 | matches | 89 | matches |
82 | } | 90 | } |
83 | 91 | ||
92 | /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match | ||
93 | /// them, while recording reasons why they don't match. This API is useful for command | ||
94 | /// line-based debugging where providing a range is difficult. | ||
95 | pub fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> { | ||
96 | use ra_db::SourceDatabaseExt; | ||
97 | let file = self.sema.parse(file_id); | ||
98 | let mut res = Vec::new(); | ||
99 | let file_text = self.sema.db.file_text(file_id); | ||
100 | let mut remaining_text = file_text.as_str(); | ||
101 | let mut base = 0; | ||
102 | let len = snippet.len() as u32; | ||
103 | while let Some(offset) = remaining_text.find(snippet) { | ||
104 | let start = base + offset as u32; | ||
105 | let end = start + len; | ||
106 | self.output_debug_for_nodes_at_range( | ||
107 | file.syntax(), | ||
108 | FileRange { file_id, range: TextRange::new(start.into(), end.into()) }, | ||
109 | &None, | ||
110 | &mut res, | ||
111 | ); | ||
112 | remaining_text = &remaining_text[offset + snippet.len()..]; | ||
113 | base = end; | ||
114 | } | ||
115 | res | ||
116 | } | ||
117 | |||
84 | fn find_matches( | 118 | fn find_matches( |
85 | &self, | 119 | &self, |
86 | code: &SyntaxNode, | 120 | code: &SyntaxNode, |
@@ -127,6 +161,59 @@ impl<'db> MatchFinder<'db> { | |||
127 | self.find_matches(&child, restrict_range, matches_out); | 161 | self.find_matches(&child, restrict_range, matches_out); |
128 | } | 162 | } |
129 | } | 163 | } |
164 | |||
165 | fn output_debug_for_nodes_at_range( | ||
166 | &self, | ||
167 | node: &SyntaxNode, | ||
168 | range: FileRange, | ||
169 | restrict_range: &Option<FileRange>, | ||
170 | out: &mut Vec<MatchDebugInfo>, | ||
171 | ) { | ||
172 | for node in node.children() { | ||
173 | let node_range = self.sema.original_range(&node); | ||
174 | if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range) | ||
175 | { | ||
176 | continue; | ||
177 | } | ||
178 | if node_range.range == range.range { | ||
179 | for rule in &self.rules { | ||
180 | let pattern = | ||
181 | rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone()); | ||
182 | out.push(MatchDebugInfo { | ||
183 | matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) | ||
184 | .map_err(|e| MatchFailureReason { | ||
185 | reason: e.reason.unwrap_or_else(|| { | ||
186 | "Match failed, but no reason was given".to_owned() | ||
187 | }), | ||
188 | }), | ||
189 | pattern, | ||
190 | node: node.clone(), | ||
191 | }); | ||
192 | } | ||
193 | } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) { | ||
194 | if let Some(expanded) = self.sema.expand(¯o_call) { | ||
195 | if let Some(tt) = macro_call.token_tree() { | ||
196 | self.output_debug_for_nodes_at_range( | ||
197 | &expanded, | ||
198 | range, | ||
199 | &Some(self.sema.original_range(tt.syntax())), | ||
200 | out, | ||
201 | ); | ||
202 | } | ||
203 | } | ||
204 | } else { | ||
205 | self.output_debug_for_nodes_at_range(&node, range, restrict_range, out); | ||
206 | } | ||
207 | } | ||
208 | } | ||
209 | } | ||
210 | |||
211 | pub struct MatchDebugInfo { | ||
212 | node: SyntaxNode, | ||
213 | /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, | ||
214 | /// etc. Will be absent if the pattern can't be parsed as that kind. | ||
215 | pattern: Result<SyntaxNode, MatchFailureReason>, | ||
216 | matched: Result<Match, MatchFailureReason>, | ||
130 | } | 217 | } |
131 | 218 | ||
132 | impl std::fmt::Display for SsrError { | 219 | impl std::fmt::Display for SsrError { |
@@ -135,4 +222,70 @@ impl std::fmt::Display for SsrError { | |||
135 | } | 222 | } |
136 | } | 223 | } |
137 | 224 | ||
225 | impl std::fmt::Debug for MatchDebugInfo { | ||
226 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
227 | write!(f, "========= PATTERN ==========\n")?; | ||
228 | match &self.pattern { | ||
229 | Ok(pattern) => { | ||
230 | write!(f, "{:#?}", pattern)?; | ||
231 | } | ||
232 | Err(err) => { | ||
233 | write!(f, "{}", err.reason)?; | ||
234 | } | ||
235 | } | ||
236 | write!( | ||
237 | f, | ||
238 | "\n============ AST ===========\n\ | ||
239 | {:#?}\n============================\n", | ||
240 | self.node | ||
241 | )?; | ||
242 | match &self.matched { | ||
243 | Ok(_) => write!(f, "Node matched")?, | ||
244 | Err(reason) => write!(f, "Node failed to match because: {}", reason.reason)?, | ||
245 | } | ||
246 | Ok(()) | ||
247 | } | ||
248 | } | ||
249 | |||
250 | impl SsrPattern { | ||
251 | fn tree_for_kind_with_reason( | ||
252 | &self, | ||
253 | kind: SyntaxKind, | ||
254 | ) -> Result<&SyntaxNode, MatchFailureReason> { | ||
255 | record_match_fails_reasons_scope(true, || self.tree_for_kind(kind)) | ||
256 | .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() }) | ||
257 | } | ||
258 | } | ||
259 | |||
260 | impl SsrMatches { | ||
261 | /// Returns `self` with any nested matches removed and made into top-level matches. | ||
262 | pub fn flattened(self) -> SsrMatches { | ||
263 | let mut out = SsrMatches::default(); | ||
264 | self.flatten_into(&mut out); | ||
265 | out | ||
266 | } | ||
267 | |||
268 | fn flatten_into(self, out: &mut SsrMatches) { | ||
269 | for mut m in self.matches { | ||
270 | for p in m.placeholder_values.values_mut() { | ||
271 | std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out); | ||
272 | } | ||
273 | out.matches.push(m); | ||
274 | } | ||
275 | } | ||
276 | } | ||
277 | |||
278 | impl Match { | ||
279 | pub fn matched_text(&self) -> String { | ||
280 | self.matched_node.text().to_string() | ||
281 | } | ||
282 | } | ||
283 | |||
138 | impl std::error::Error for SsrError {} | 284 | impl std::error::Error for SsrError {} |
285 | |||
286 | #[cfg(test)] | ||
287 | impl MatchDebugInfo { | ||
288 | pub(crate) fn match_failure_reason(&self) -> Option<&str> { | ||
289 | self.matched.as_ref().err().map(|r| r.reason.as_str()) | ||
290 | } | ||
291 | } | ||
diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs index 85420ed3c..53d802e77 100644 --- a/crates/ra_ssr/src/matching.rs +++ b/crates/ra_ssr/src/matching.rs | |||
@@ -8,9 +8,7 @@ use crate::{ | |||
8 | use hir::Semantics; | 8 | use hir::Semantics; |
9 | use ra_db::FileRange; | 9 | use ra_db::FileRange; |
10 | use ra_syntax::ast::{AstNode, AstToken}; | 10 | use ra_syntax::ast::{AstNode, AstToken}; |
11 | use ra_syntax::{ | 11 | use ra_syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken}; |
12 | ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, | ||
13 | }; | ||
14 | use rustc_hash::FxHashMap; | 12 | use rustc_hash::FxHashMap; |
15 | use std::{cell::Cell, iter::Peekable}; | 13 | use std::{cell::Cell, iter::Peekable}; |
16 | 14 | ||
@@ -44,8 +42,8 @@ macro_rules! fail_match { | |||
44 | 42 | ||
45 | /// Information about a match that was found. | 43 | /// Information about a match that was found. |
46 | #[derive(Debug)] | 44 | #[derive(Debug)] |
47 | pub(crate) struct Match { | 45 | pub struct Match { |
48 | pub(crate) range: TextRange, | 46 | pub(crate) range: FileRange, |
49 | pub(crate) matched_node: SyntaxNode, | 47 | pub(crate) matched_node: SyntaxNode, |
50 | pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>, | 48 | pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>, |
51 | pub(crate) ignored_comments: Vec<ast::Comment>, | 49 | pub(crate) ignored_comments: Vec<ast::Comment>, |
@@ -135,7 +133,7 @@ impl<'db, 'sema> MatchState<'db, 'sema> { | |||
135 | match_state.attempt_match_node(&match_inputs, &pattern_tree, code)?; | 133 | match_state.attempt_match_node(&match_inputs, &pattern_tree, code)?; |
136 | match_state.validate_range(&sema.original_range(code))?; | 134 | match_state.validate_range(&sema.original_range(code))?; |
137 | match_state.match_out = Some(Match { | 135 | match_state.match_out = Some(Match { |
138 | range: sema.original_range(code).range, | 136 | range: sema.original_range(code), |
139 | matched_node: code.clone(), | 137 | matched_node: code.clone(), |
140 | placeholder_values: FxHashMap::default(), | 138 | placeholder_values: FxHashMap::default(), |
141 | ignored_comments: Vec::new(), | 139 | ignored_comments: Vec::new(), |
@@ -225,7 +223,7 @@ impl<'db, 'sema> MatchState<'db, 'sema> { | |||
225 | match self.next_non_trivial(&mut code_it) { | 223 | match self.next_non_trivial(&mut code_it) { |
226 | None => { | 224 | None => { |
227 | if let Some(p) = pattern_it.next() { | 225 | if let Some(p) = pattern_it.next() { |
228 | fail_match!("Part of the pattern was unmached: {:?}", p); | 226 | fail_match!("Part of the pattern was unmatched: {:?}", p); |
229 | } | 227 | } |
230 | return Ok(()); | 228 | return Ok(()); |
231 | } | 229 | } |
@@ -585,7 +583,7 @@ mod tests { | |||
585 | "1+2" | 583 | "1+2" |
586 | ); | 584 | ); |
587 | 585 | ||
588 | let edit = crate::replacing::matches_to_edit(&matches); | 586 | let edit = crate::replacing::matches_to_edit(&matches, input); |
589 | let mut after = input.to_string(); | 587 | let mut after = input.to_string(); |
590 | edit.apply(&mut after); | 588 | edit.apply(&mut after); |
591 | assert_eq!(after, "fn main() { bar(1+2); }"); | 589 | assert_eq!(after, "fn main() { bar(1+2); }"); |
diff --git a/crates/ra_ssr/src/parsing.rs b/crates/ra_ssr/src/parsing.rs index 90c13dbc2..04d46bd32 100644 --- a/crates/ra_ssr/src/parsing.rs +++ b/crates/ra_ssr/src/parsing.rs | |||
@@ -55,7 +55,7 @@ impl FromStr for SsrRule { | |||
55 | let pattern = it.next().expect("at least empty string").trim(); | 55 | let pattern = it.next().expect("at least empty string").trim(); |
56 | let template = it | 56 | let template = it |
57 | .next() | 57 | .next() |
58 | .ok_or_else(|| SsrError("Cannot find delemiter `==>>`".into()))? | 58 | .ok_or_else(|| SsrError("Cannot find delimiter `==>>`".into()))? |
59 | .trim() | 59 | .trim() |
60 | .to_string(); | 60 | .to_string(); |
61 | if it.next().is_some() { | 61 | if it.next().is_some() { |
@@ -165,7 +165,7 @@ fn parse_pattern(pattern_str: &str) -> Result<Vec<PatternElement>, SsrError> { | |||
165 | /// Checks for errors in a rule. e.g. the replace pattern referencing placeholders that the search | 165 | /// Checks for errors in a rule. e.g. the replace pattern referencing placeholders that the search |
166 | /// pattern didn't define. | 166 | /// pattern didn't define. |
167 | fn validate_rule(rule: &SsrRule) -> Result<(), SsrError> { | 167 | fn validate_rule(rule: &SsrRule) -> Result<(), SsrError> { |
168 | let mut defined_placeholders = std::collections::HashSet::new(); | 168 | let mut defined_placeholders = FxHashSet::default(); |
169 | for p in &rule.pattern.raw.tokens { | 169 | for p in &rule.pattern.raw.tokens { |
170 | if let PatternElement::Placeholder(placeholder) = p { | 170 | if let PatternElement::Placeholder(placeholder) = p { |
171 | defined_placeholders.insert(&placeholder.ident); | 171 | defined_placeholders.insert(&placeholder.ident); |
diff --git a/crates/ra_ssr/src/replacing.rs b/crates/ra_ssr/src/replacing.rs index 5dcde82a2..e43cc5167 100644 --- a/crates/ra_ssr/src/replacing.rs +++ b/crates/ra_ssr/src/replacing.rs | |||
@@ -10,21 +10,27 @@ use ra_text_edit::TextEdit; | |||
10 | /// Returns a text edit that will replace each match in `matches` with its corresponding replacement | 10 | /// Returns a text edit that will replace each match in `matches` with its corresponding replacement |
11 | /// template. Placeholders in the template will have been substituted with whatever they matched to | 11 | /// template. Placeholders in the template will have been substituted with whatever they matched to |
12 | /// in the original code. | 12 | /// in the original code. |
13 | pub(crate) fn matches_to_edit(matches: &SsrMatches) -> TextEdit { | 13 | pub(crate) fn matches_to_edit(matches: &SsrMatches, file_src: &str) -> TextEdit { |
14 | matches_to_edit_at_offset(matches, 0.into()) | 14 | matches_to_edit_at_offset(matches, file_src, 0.into()) |
15 | } | 15 | } |
16 | 16 | ||
17 | fn matches_to_edit_at_offset(matches: &SsrMatches, relative_start: TextSize) -> TextEdit { | 17 | fn matches_to_edit_at_offset( |
18 | matches: &SsrMatches, | ||
19 | file_src: &str, | ||
20 | relative_start: TextSize, | ||
21 | ) -> TextEdit { | ||
18 | let mut edit_builder = ra_text_edit::TextEditBuilder::default(); | 22 | let mut edit_builder = ra_text_edit::TextEditBuilder::default(); |
19 | for m in &matches.matches { | 23 | for m in &matches.matches { |
20 | edit_builder.replace(m.range.checked_sub(relative_start).unwrap(), render_replace(m)); | 24 | edit_builder.replace( |
25 | m.range.range.checked_sub(relative_start).unwrap(), | ||
26 | render_replace(m, file_src), | ||
27 | ); | ||
21 | } | 28 | } |
22 | edit_builder.finish() | 29 | edit_builder.finish() |
23 | } | 30 | } |
24 | 31 | ||
25 | fn render_replace(match_info: &Match) -> String { | 32 | fn render_replace(match_info: &Match, file_src: &str) -> String { |
26 | let mut out = String::new(); | 33 | let mut out = String::new(); |
27 | let match_start = match_info.matched_node.text_range().start(); | ||
28 | for r in &match_info.template.tokens { | 34 | for r in &match_info.template.tokens { |
29 | match r { | 35 | match r { |
30 | PatternElement::Token(t) => out.push_str(t.text.as_str()), | 36 | PatternElement::Token(t) => out.push_str(t.text.as_str()), |
@@ -33,16 +39,13 @@ fn render_replace(match_info: &Match) -> String { | |||
33 | match_info.placeholder_values.get(&Var(p.ident.to_string())) | 39 | match_info.placeholder_values.get(&Var(p.ident.to_string())) |
34 | { | 40 | { |
35 | let range = &placeholder_value.range.range; | 41 | let range = &placeholder_value.range.range; |
36 | let mut matched_text = if let Some(node) = &placeholder_value.node { | 42 | let mut matched_text = |
37 | node.text().to_string() | 43 | file_src[usize::from(range.start())..usize::from(range.end())].to_owned(); |
38 | } else { | 44 | let edit = matches_to_edit_at_offset( |
39 | let relative_range = range.checked_sub(match_start).unwrap(); | 45 | &placeholder_value.inner_matches, |
40 | match_info.matched_node.text().to_string() | 46 | file_src, |
41 | [usize::from(relative_range.start())..usize::from(relative_range.end())] | 47 | range.start(), |
42 | .to_string() | 48 | ); |
43 | }; | ||
44 | let edit = | ||
45 | matches_to_edit_at_offset(&placeholder_value.inner_matches, range.start()); | ||
46 | edit.apply(&mut matched_text); | 49 | edit.apply(&mut matched_text); |
47 | out.push_str(&matched_text); | 50 | out.push_str(&matched_text); |
48 | } else { | 51 | } else { |
diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs index 7a3141be8..c692c97e2 100644 --- a/crates/ra_ssr/src/tests.rs +++ b/crates/ra_ssr/src/tests.rs | |||
@@ -1,150 +1,5 @@ | |||
1 | use crate::matching::MatchFailureReason; | 1 | use crate::{MatchFinder, SsrRule}; |
2 | use crate::{matching, Match, MatchFinder, SsrMatches, SsrPattern, SsrRule}; | 2 | use ra_db::{FileId, SourceDatabaseExt}; |
3 | use matching::record_match_fails_reasons_scope; | ||
4 | use ra_db::{FileId, FileRange, SourceDatabaseExt}; | ||
5 | use ra_syntax::ast::AstNode; | ||
6 | use ra_syntax::{ast, SyntaxKind, SyntaxNode, TextRange}; | ||
7 | |||
8 | struct MatchDebugInfo { | ||
9 | node: SyntaxNode, | ||
10 | /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, | ||
11 | /// etc. Will be absent if the pattern can't be parsed as that kind. | ||
12 | pattern: Result<SyntaxNode, MatchFailureReason>, | ||
13 | matched: Result<Match, MatchFailureReason>, | ||
14 | } | ||
15 | |||
16 | impl SsrPattern { | ||
17 | pub(crate) fn tree_for_kind_with_reason( | ||
18 | &self, | ||
19 | kind: SyntaxKind, | ||
20 | ) -> Result<&SyntaxNode, MatchFailureReason> { | ||
21 | record_match_fails_reasons_scope(true, || self.tree_for_kind(kind)) | ||
22 | .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() }) | ||
23 | } | ||
24 | } | ||
25 | |||
26 | impl std::fmt::Debug for MatchDebugInfo { | ||
27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
28 | write!(f, "========= PATTERN ==========\n")?; | ||
29 | match &self.pattern { | ||
30 | Ok(pattern) => { | ||
31 | write!(f, "{:#?}", pattern)?; | ||
32 | } | ||
33 | Err(err) => { | ||
34 | write!(f, "{}", err.reason)?; | ||
35 | } | ||
36 | } | ||
37 | write!( | ||
38 | f, | ||
39 | "\n============ AST ===========\n\ | ||
40 | {:#?}\n============================", | ||
41 | self.node | ||
42 | )?; | ||
43 | match &self.matched { | ||
44 | Ok(_) => write!(f, "Node matched")?, | ||
45 | Err(reason) => write!(f, "Node failed to match because: {}", reason.reason)?, | ||
46 | } | ||
47 | Ok(()) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | impl SsrMatches { | ||
52 | /// Returns `self` with any nested matches removed and made into top-level matches. | ||
53 | pub(crate) fn flattened(self) -> SsrMatches { | ||
54 | let mut out = SsrMatches::default(); | ||
55 | self.flatten_into(&mut out); | ||
56 | out | ||
57 | } | ||
58 | |||
59 | fn flatten_into(self, out: &mut SsrMatches) { | ||
60 | for mut m in self.matches { | ||
61 | for p in m.placeholder_values.values_mut() { | ||
62 | std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out); | ||
63 | } | ||
64 | out.matches.push(m); | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | |||
69 | impl Match { | ||
70 | pub(crate) fn matched_text(&self) -> String { | ||
71 | self.matched_node.text().to_string() | ||
72 | } | ||
73 | } | ||
74 | |||
75 | impl<'db> MatchFinder<'db> { | ||
76 | /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you | ||
77 | /// intend to do replacement, use `add_rule` instead. | ||
78 | fn add_search_pattern(&mut self, pattern: SsrPattern) { | ||
79 | self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) | ||
80 | } | ||
81 | |||
82 | /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match | ||
83 | /// them, while recording reasons why they don't match. This API is useful for command | ||
84 | /// line-based debugging where providing a range is difficult. | ||
85 | fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> { | ||
86 | let file = self.sema.parse(file_id); | ||
87 | let mut res = Vec::new(); | ||
88 | let file_text = self.sema.db.file_text(file_id); | ||
89 | let mut remaining_text = file_text.as_str(); | ||
90 | let mut base = 0; | ||
91 | let len = snippet.len() as u32; | ||
92 | while let Some(offset) = remaining_text.find(snippet) { | ||
93 | let start = base + offset as u32; | ||
94 | let end = start + len; | ||
95 | self.output_debug_for_nodes_at_range( | ||
96 | file.syntax(), | ||
97 | TextRange::new(start.into(), end.into()), | ||
98 | &None, | ||
99 | &mut res, | ||
100 | ); | ||
101 | remaining_text = &remaining_text[offset + snippet.len()..]; | ||
102 | base = end; | ||
103 | } | ||
104 | res | ||
105 | } | ||
106 | |||
107 | fn output_debug_for_nodes_at_range( | ||
108 | &self, | ||
109 | node: &SyntaxNode, | ||
110 | range: TextRange, | ||
111 | restrict_range: &Option<FileRange>, | ||
112 | out: &mut Vec<MatchDebugInfo>, | ||
113 | ) { | ||
114 | for node in node.children() { | ||
115 | if !node.text_range().contains_range(range) { | ||
116 | continue; | ||
117 | } | ||
118 | if node.text_range() == range { | ||
119 | for rule in &self.rules { | ||
120 | let pattern = | ||
121 | rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone()); | ||
122 | out.push(MatchDebugInfo { | ||
123 | matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) | ||
124 | .map_err(|e| MatchFailureReason { | ||
125 | reason: e.reason.unwrap_or_else(|| { | ||
126 | "Match failed, but no reason was given".to_owned() | ||
127 | }), | ||
128 | }), | ||
129 | pattern, | ||
130 | node: node.clone(), | ||
131 | }); | ||
132 | } | ||
133 | } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) { | ||
134 | if let Some(expanded) = self.sema.expand(¯o_call) { | ||
135 | if let Some(tt) = macro_call.token_tree() { | ||
136 | self.output_debug_for_nodes_at_range( | ||
137 | &expanded, | ||
138 | range, | ||
139 | &Some(self.sema.original_range(tt.syntax())), | ||
140 | out, | ||
141 | ); | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | } | ||
146 | } | ||
147 | } | ||
148 | 3 | ||
149 | fn parse_error_text(query: &str) -> String { | 4 | fn parse_error_text(query: &str) -> String { |
150 | format!("{}", query.parse::<SsrRule>().unwrap_err()) | 5 | format!("{}", query.parse::<SsrRule>().unwrap_err()) |
@@ -152,12 +7,12 @@ fn parse_error_text(query: &str) -> String { | |||
152 | 7 | ||
153 | #[test] | 8 | #[test] |
154 | fn parser_empty_query() { | 9 | fn parser_empty_query() { |
155 | assert_eq!(parse_error_text(""), "Parse error: Cannot find delemiter `==>>`"); | 10 | assert_eq!(parse_error_text(""), "Parse error: Cannot find delimiter `==>>`"); |
156 | } | 11 | } |
157 | 12 | ||
158 | #[test] | 13 | #[test] |
159 | fn parser_no_delimiter() { | 14 | fn parser_no_delimiter() { |
160 | assert_eq!(parse_error_text("foo()"), "Parse error: Cannot find delemiter `==>>`"); | 15 | assert_eq!(parse_error_text("foo()"), "Parse error: Cannot find delimiter `==>>`"); |
161 | } | 16 | } |
162 | 17 | ||
163 | #[test] | 18 | #[test] |
@@ -227,7 +82,7 @@ fn assert_ssr_transforms(rules: &[&str], input: &str, result: &str) { | |||
227 | let mut after = db.file_text(file_id).to_string(); | 82 | let mut after = db.file_text(file_id).to_string(); |
228 | edits.apply(&mut after); | 83 | edits.apply(&mut after); |
229 | // Likewise, we need to make sure that whatever transformations fixture parsing applies, | 84 | // Likewise, we need to make sure that whatever transformations fixture parsing applies, |
230 | // also get appplied to our expected result. | 85 | // also get applied to our expected result. |
231 | let result = normalize_code(result); | 86 | let result = normalize_code(result); |
232 | assert_eq!(after, result); | 87 | assert_eq!(after, result); |
233 | } else { | 88 | } else { |
@@ -260,6 +115,19 @@ fn assert_no_match(pattern: &str, code: &str) { | |||
260 | assert_matches(pattern, code, &[]); | 115 | assert_matches(pattern, code, &[]); |
261 | } | 116 | } |
262 | 117 | ||
118 | fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) { | ||
119 | let (db, file_id) = single_file(code); | ||
120 | let mut match_finder = MatchFinder::new(&db); | ||
121 | match_finder.add_search_pattern(pattern.parse().unwrap()); | ||
122 | let mut reasons = Vec::new(); | ||
123 | for d in match_finder.debug_where_text_equal(file_id, snippet) { | ||
124 | if let Some(reason) = d.match_failure_reason() { | ||
125 | reasons.push(reason.to_owned()); | ||
126 | } | ||
127 | } | ||
128 | assert_eq!(reasons, vec![expected_reason]); | ||
129 | } | ||
130 | |||
263 | #[test] | 131 | #[test] |
264 | fn ssr_function_to_method() { | 132 | fn ssr_function_to_method() { |
265 | assert_ssr_transform( | 133 | assert_ssr_transform( |
@@ -606,3 +474,47 @@ fn replace_within_macro_expansion() { | |||
606 | fn f() {macro1!(bar(5.x()).o2())}"#, | 474 | fn f() {macro1!(bar(5.x()).o2())}"#, |
607 | ) | 475 | ) |
608 | } | 476 | } |
477 | |||
478 | #[test] | ||
479 | fn preserves_whitespace_within_macro_expansion() { | ||
480 | assert_ssr_transform( | ||
481 | "$a + $b ==>> $b - $a", | ||
482 | r#" | ||
483 | macro_rules! macro1 { | ||
484 | ($a:expr) => {$a} | ||
485 | } | ||
486 | fn f() {macro1!(1 * 2 + 3 + 4}"#, | ||
487 | r#" | ||
488 | macro_rules! macro1 { | ||
489 | ($a:expr) => {$a} | ||
490 | } | ||
491 | fn f() {macro1!(4 - 3 - 1 * 2}"#, | ||
492 | ) | ||
493 | } | ||
494 | |||
495 | #[test] | ||
496 | fn match_failure_reasons() { | ||
497 | let code = r#" | ||
498 | macro_rules! foo { | ||
499 | ($a:expr) => { | ||
500 | 1 + $a + 2 | ||
501 | }; | ||
502 | } | ||
503 | fn f1() { | ||
504 | bar(1, 2); | ||
505 | foo!(5 + 43.to_string() + 5); | ||
506 | } | ||
507 | "#; | ||
508 | assert_match_failure_reason( | ||
509 | "bar($a, 3)", | ||
510 | code, | ||
511 | "bar(1, 2)", | ||
512 | r#"Pattern wanted token '3' (INT_NUMBER), but code had token '2' (INT_NUMBER)"#, | ||
513 | ); | ||
514 | assert_match_failure_reason( | ||
515 | "42.to_string()", | ||
516 | code, | ||
517 | "43.to_string()", | ||
518 | r#"Pattern wanted token '42' (INT_NUMBER), but code had token '43' (INT_NUMBER)"#, | ||
519 | ); | ||
520 | } | ||