diff options
Diffstat (limited to 'crates/ra_ssr/src/lib.rs')
-rw-r--r-- | crates/ra_ssr/src/lib.rs | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs new file mode 100644 index 000000000..cca4576ce --- /dev/null +++ b/crates/ra_ssr/src/lib.rs | |||
@@ -0,0 +1,285 @@ | |||
1 | //! Structural Search Replace | ||
2 | //! | ||
3 | //! Allows searching the AST for code that matches one or more patterns and then replacing that code | ||
4 | //! based on a template. | ||
5 | |||
6 | mod matching; | ||
7 | mod parsing; | ||
8 | mod replacing; | ||
9 | #[macro_use] | ||
10 | mod errors; | ||
11 | #[cfg(test)] | ||
12 | mod tests; | ||
13 | |||
14 | pub use crate::errors::SsrError; | ||
15 | pub use crate::matching::Match; | ||
16 | use crate::matching::{record_match_fails_reasons_scope, MatchFailureReason}; | ||
17 | use hir::Semantics; | ||
18 | use ra_db::{FileId, FileRange}; | ||
19 | use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, TextRange}; | ||
20 | use ra_text_edit::TextEdit; | ||
21 | use rustc_hash::FxHashMap; | ||
22 | |||
23 | // A structured search replace rule. Create by calling `parse` on a str. | ||
24 | #[derive(Debug)] | ||
25 | pub struct SsrRule { | ||
26 | /// A structured pattern that we're searching for. | ||
27 | pattern: SsrPattern, | ||
28 | /// What we'll replace it with. | ||
29 | template: parsing::SsrTemplate, | ||
30 | } | ||
31 | |||
32 | #[derive(Debug)] | ||
33 | pub struct SsrPattern { | ||
34 | raw: parsing::RawSearchPattern, | ||
35 | /// Placeholders keyed by the stand-in ident that we use in Rust source code. | ||
36 | placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>, | ||
37 | // We store our search pattern, parsed as each different kind of thing we can look for. As we | ||
38 | // traverse the AST, we get the appropriate one of these for the type of node we're on. For many | ||
39 | // search patterns, only some of these will be present. | ||
40 | expr: Option<SyntaxNode>, | ||
41 | type_ref: Option<SyntaxNode>, | ||
42 | item: Option<SyntaxNode>, | ||
43 | path: Option<SyntaxNode>, | ||
44 | pattern: Option<SyntaxNode>, | ||
45 | } | ||
46 | |||
47 | #[derive(Debug, Default)] | ||
48 | pub struct SsrMatches { | ||
49 | pub matches: Vec<Match>, | ||
50 | } | ||
51 | |||
52 | /// Searches a crate for pattern matches and possibly replaces them with something else. | ||
53 | pub struct MatchFinder<'db> { | ||
54 | /// Our source of information about the user's code. | ||
55 | sema: Semantics<'db, ra_ide_db::RootDatabase>, | ||
56 | rules: Vec<SsrRule>, | ||
57 | } | ||
58 | |||
59 | impl<'db> MatchFinder<'db> { | ||
60 | pub fn new(db: &'db ra_ide_db::RootDatabase) -> MatchFinder<'db> { | ||
61 | MatchFinder { sema: Semantics::new(db), rules: Vec::new() } | ||
62 | } | ||
63 | |||
64 | pub fn add_rule(&mut self, rule: SsrRule) { | ||
65 | self.rules.push(rule); | ||
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 | |||
74 | pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { | ||
75 | let matches = self.find_matches_in_file(file_id); | ||
76 | if matches.matches.is_empty() { | ||
77 | None | ||
78 | } else { | ||
79 | use ra_db::SourceDatabaseExt; | ||
80 | Some(replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id))) | ||
81 | } | ||
82 | } | ||
83 | |||
84 | pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { | ||
85 | let file = self.sema.parse(file_id); | ||
86 | let code = file.syntax(); | ||
87 | let mut matches = SsrMatches::default(); | ||
88 | self.find_matches(code, &None, &mut matches); | ||
89 | matches | ||
90 | } | ||
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 | |||
118 | fn find_matches( | ||
119 | &self, | ||
120 | code: &SyntaxNode, | ||
121 | restrict_range: &Option<FileRange>, | ||
122 | matches_out: &mut SsrMatches, | ||
123 | ) { | ||
124 | for rule in &self.rules { | ||
125 | if let Ok(mut m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) { | ||
126 | // Continue searching in each of our placeholders. | ||
127 | for placeholder_value in m.placeholder_values.values_mut() { | ||
128 | if let Some(placeholder_node) = &placeholder_value.node { | ||
129 | // Don't search our placeholder if it's the entire matched node, otherwise we'd | ||
130 | // find the same match over and over until we got a stack overflow. | ||
131 | if placeholder_node != code { | ||
132 | self.find_matches( | ||
133 | placeholder_node, | ||
134 | restrict_range, | ||
135 | &mut placeholder_value.inner_matches, | ||
136 | ); | ||
137 | } | ||
138 | } | ||
139 | } | ||
140 | matches_out.matches.push(m); | ||
141 | return; | ||
142 | } | ||
143 | } | ||
144 | // If we've got a macro call, we already tried matching it pre-expansion, which is the only | ||
145 | // way to match the whole macro, now try expanding it and matching the expansion. | ||
146 | if let Some(macro_call) = ast::MacroCall::cast(code.clone()) { | ||
147 | if let Some(expanded) = self.sema.expand(¯o_call) { | ||
148 | if let Some(tt) = macro_call.token_tree() { | ||
149 | // When matching within a macro expansion, we only want to allow matches of | ||
150 | // nodes that originated entirely from within the token tree of the macro call. | ||
151 | // i.e. we don't want to match something that came from the macro itself. | ||
152 | self.find_matches( | ||
153 | &expanded, | ||
154 | &Some(self.sema.original_range(tt.syntax())), | ||
155 | matches_out, | ||
156 | ); | ||
157 | } | ||
158 | } | ||
159 | } | ||
160 | for child in code.children() { | ||
161 | self.find_matches(&child, restrict_range, matches_out); | ||
162 | } | ||
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 | } | ||
205 | self.output_debug_for_nodes_at_range(&node, range, restrict_range, out); | ||
206 | } | ||
207 | } | ||
208 | } | ||
209 | |||
210 | pub struct MatchDebugInfo { | ||
211 | node: SyntaxNode, | ||
212 | /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, | ||
213 | /// etc. Will be absent if the pattern can't be parsed as that kind. | ||
214 | pattern: Result<SyntaxNode, MatchFailureReason>, | ||
215 | matched: Result<Match, MatchFailureReason>, | ||
216 | } | ||
217 | |||
218 | impl std::fmt::Debug for MatchDebugInfo { | ||
219 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
220 | match &self.matched { | ||
221 | Ok(_) => writeln!(f, "Node matched")?, | ||
222 | Err(reason) => writeln!(f, "Node failed to match because: {}", reason.reason)?, | ||
223 | } | ||
224 | writeln!( | ||
225 | f, | ||
226 | "============ AST ===========\n\ | ||
227 | {:#?}", | ||
228 | self.node | ||
229 | )?; | ||
230 | writeln!(f, "========= PATTERN ==========")?; | ||
231 | match &self.pattern { | ||
232 | Ok(pattern) => { | ||
233 | writeln!(f, "{:#?}", pattern)?; | ||
234 | } | ||
235 | Err(err) => { | ||
236 | writeln!(f, "{}", err.reason)?; | ||
237 | } | ||
238 | } | ||
239 | writeln!(f, "============================")?; | ||
240 | Ok(()) | ||
241 | } | ||
242 | } | ||
243 | |||
244 | impl SsrPattern { | ||
245 | fn tree_for_kind_with_reason( | ||
246 | &self, | ||
247 | kind: SyntaxKind, | ||
248 | ) -> Result<&SyntaxNode, MatchFailureReason> { | ||
249 | record_match_fails_reasons_scope(true, || self.tree_for_kind(kind)) | ||
250 | .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() }) | ||
251 | } | ||
252 | } | ||
253 | |||
254 | impl SsrMatches { | ||
255 | /// Returns `self` with any nested matches removed and made into top-level matches. | ||
256 | pub fn flattened(self) -> SsrMatches { | ||
257 | let mut out = SsrMatches::default(); | ||
258 | self.flatten_into(&mut out); | ||
259 | out | ||
260 | } | ||
261 | |||
262 | fn flatten_into(self, out: &mut SsrMatches) { | ||
263 | for mut m in self.matches { | ||
264 | for p in m.placeholder_values.values_mut() { | ||
265 | std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out); | ||
266 | } | ||
267 | out.matches.push(m); | ||
268 | } | ||
269 | } | ||
270 | } | ||
271 | |||
272 | impl Match { | ||
273 | pub fn matched_text(&self) -> String { | ||
274 | self.matched_node.text().to_string() | ||
275 | } | ||
276 | } | ||
277 | |||
278 | impl std::error::Error for SsrError {} | ||
279 | |||
280 | #[cfg(test)] | ||
281 | impl MatchDebugInfo { | ||
282 | pub(crate) fn match_failure_reason(&self) -> Option<&str> { | ||
283 | self.matched.as_ref().err().map(|r| r.reason.as_str()) | ||
284 | } | ||
285 | } | ||