diff options
Diffstat (limited to 'crates/ssr/src/lib.rs')
-rw-r--r-- | crates/ssr/src/lib.rs | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/crates/ssr/src/lib.rs b/crates/ssr/src/lib.rs new file mode 100644 index 000000000..ba669fd56 --- /dev/null +++ b/crates/ssr/src/lib.rs | |||
@@ -0,0 +1,341 @@ | |||
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 | // Feature: Structural Search and Replace | ||
7 | // | ||
8 | // Search and replace with named wildcards that will match any expression, type, path, pattern or item. | ||
9 | // The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`. | ||
10 | // A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement. | ||
11 | // Within a macro call, a placeholder will match up until whatever token follows the placeholder. | ||
12 | // | ||
13 | // All paths in both the search pattern and the replacement template must resolve in the context | ||
14 | // in which this command is invoked. Paths in the search pattern will then match the code if they | ||
15 | // resolve to the same item, even if they're written differently. For example if we invoke the | ||
16 | // command in the module `foo` with a pattern of `Bar`, then code in the parent module that refers | ||
17 | // to `foo::Bar` will match. | ||
18 | // | ||
19 | // Paths in the replacement template will be rendered appropriately for the context in which the | ||
20 | // replacement occurs. For example if our replacement template is `foo::Bar` and we match some | ||
21 | // code in the `foo` module, we'll insert just `Bar`. | ||
22 | // | ||
23 | // Inherent method calls should generally be written in UFCS form. e.g. `foo::Bar::baz($s, $a)` will | ||
24 | // match `$s.baz($a)`, provided the method call `baz` resolves to the method `foo::Bar::baz`. When a | ||
25 | // placeholder is the receiver of a method call in the search pattern (e.g. `$s.foo()`), but not in | ||
26 | // the replacement template (e.g. `bar($s)`), then *, & and &mut will be added as needed to mirror | ||
27 | // whatever autoderef and autoref was happening implicitly in the matched code. | ||
28 | // | ||
29 | // The scope of the search / replace will be restricted to the current selection if any, otherwise | ||
30 | // it will apply to the whole workspace. | ||
31 | // | ||
32 | // Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`. | ||
33 | // | ||
34 | // Supported constraints: | ||
35 | // | ||
36 | // |=== | ||
37 | // | Constraint | Restricts placeholder | ||
38 | // | ||
39 | // | kind(literal) | Is a literal (e.g. `42` or `"forty two"`) | ||
40 | // | not(a) | Negates the constraint `a` | ||
41 | // |=== | ||
42 | // | ||
43 | // Available via the command `rust-analyzer.ssr`. | ||
44 | // | ||
45 | // ```rust | ||
46 | // // Using structural search replace command [foo($a, $b) ==>> ($a).foo($b)] | ||
47 | // | ||
48 | // // BEFORE | ||
49 | // String::from(foo(y + 5, z)) | ||
50 | // | ||
51 | // // AFTER | ||
52 | // String::from((y + 5).foo(z)) | ||
53 | // ``` | ||
54 | // | ||
55 | // |=== | ||
56 | // | Editor | Action Name | ||
57 | // | ||
58 | // | VS Code | **Rust Analyzer: Structural Search Replace** | ||
59 | // |=== | ||
60 | |||
61 | mod matching; | ||
62 | mod nester; | ||
63 | mod parsing; | ||
64 | mod replacing; | ||
65 | mod resolving; | ||
66 | mod search; | ||
67 | #[macro_use] | ||
68 | mod errors; | ||
69 | #[cfg(test)] | ||
70 | mod tests; | ||
71 | |||
72 | use crate::errors::bail; | ||
73 | pub use crate::errors::SsrError; | ||
74 | pub use crate::matching::Match; | ||
75 | use crate::matching::MatchFailureReason; | ||
76 | use base_db::{FileId, FilePosition, FileRange}; | ||
77 | use hir::Semantics; | ||
78 | use ide_db::source_change::SourceFileEdit; | ||
79 | use resolving::ResolvedRule; | ||
80 | use rustc_hash::FxHashMap; | ||
81 | use syntax::{ast, AstNode, SyntaxNode, TextRange}; | ||
82 | |||
83 | // A structured search replace rule. Create by calling `parse` on a str. | ||
84 | #[derive(Debug)] | ||
85 | pub struct SsrRule { | ||
86 | /// A structured pattern that we're searching for. | ||
87 | pattern: parsing::RawPattern, | ||
88 | /// What we'll replace it with. | ||
89 | template: parsing::RawPattern, | ||
90 | parsed_rules: Vec<parsing::ParsedRule>, | ||
91 | } | ||
92 | |||
93 | #[derive(Debug)] | ||
94 | pub struct SsrPattern { | ||
95 | raw: parsing::RawPattern, | ||
96 | parsed_rules: Vec<parsing::ParsedRule>, | ||
97 | } | ||
98 | |||
99 | #[derive(Debug, Default)] | ||
100 | pub struct SsrMatches { | ||
101 | pub matches: Vec<Match>, | ||
102 | } | ||
103 | |||
104 | /// Searches a crate for pattern matches and possibly replaces them with something else. | ||
105 | pub struct MatchFinder<'db> { | ||
106 | /// Our source of information about the user's code. | ||
107 | sema: Semantics<'db, ide_db::RootDatabase>, | ||
108 | rules: Vec<ResolvedRule>, | ||
109 | resolution_scope: resolving::ResolutionScope<'db>, | ||
110 | restrict_ranges: Vec<FileRange>, | ||
111 | } | ||
112 | |||
113 | impl<'db> MatchFinder<'db> { | ||
114 | /// Constructs a new instance where names will be looked up as if they appeared at | ||
115 | /// `lookup_context`. | ||
116 | pub fn in_context( | ||
117 | db: &'db ide_db::RootDatabase, | ||
118 | lookup_context: FilePosition, | ||
119 | mut restrict_ranges: Vec<FileRange>, | ||
120 | ) -> MatchFinder<'db> { | ||
121 | restrict_ranges.retain(|range| !range.range.is_empty()); | ||
122 | let sema = Semantics::new(db); | ||
123 | let resolution_scope = resolving::ResolutionScope::new(&sema, lookup_context); | ||
124 | MatchFinder { sema, rules: Vec::new(), resolution_scope, restrict_ranges } | ||
125 | } | ||
126 | |||
127 | /// Constructs an instance using the start of the first file in `db` as the lookup context. | ||
128 | pub fn at_first_file(db: &'db ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> { | ||
129 | use base_db::SourceDatabaseExt; | ||
130 | use ide_db::symbol_index::SymbolsDatabase; | ||
131 | if let Some(first_file_id) = db | ||
132 | .local_roots() | ||
133 | .iter() | ||
134 | .next() | ||
135 | .and_then(|root| db.source_root(root.clone()).iter().next()) | ||
136 | { | ||
137 | Ok(MatchFinder::in_context( | ||
138 | db, | ||
139 | FilePosition { file_id: first_file_id, offset: 0.into() }, | ||
140 | vec![], | ||
141 | )) | ||
142 | } else { | ||
143 | bail!("No files to search"); | ||
144 | } | ||
145 | } | ||
146 | |||
147 | /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take | ||
148 | /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to | ||
149 | /// match to it. | ||
150 | pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> { | ||
151 | for parsed_rule in rule.parsed_rules { | ||
152 | self.rules.push(ResolvedRule::new( | ||
153 | parsed_rule, | ||
154 | &self.resolution_scope, | ||
155 | self.rules.len(), | ||
156 | )?); | ||
157 | } | ||
158 | Ok(()) | ||
159 | } | ||
160 | |||
161 | /// Finds matches for all added rules and returns edits for all found matches. | ||
162 | pub fn edits(&self) -> Vec<SourceFileEdit> { | ||
163 | use base_db::SourceDatabaseExt; | ||
164 | let mut matches_by_file = FxHashMap::default(); | ||
165 | for m in self.matches().matches { | ||
166 | matches_by_file | ||
167 | .entry(m.range.file_id) | ||
168 | .or_insert_with(|| SsrMatches::default()) | ||
169 | .matches | ||
170 | .push(m); | ||
171 | } | ||
172 | let mut edits = vec![]; | ||
173 | for (file_id, matches) in matches_by_file { | ||
174 | let edit = | ||
175 | replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules); | ||
176 | edits.push(SourceFileEdit { file_id, edit }); | ||
177 | } | ||
178 | edits | ||
179 | } | ||
180 | |||
181 | /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you | ||
182 | /// intend to do replacement, use `add_rule` instead. | ||
183 | pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> { | ||
184 | for parsed_rule in pattern.parsed_rules { | ||
185 | self.rules.push(ResolvedRule::new( | ||
186 | parsed_rule, | ||
187 | &self.resolution_scope, | ||
188 | self.rules.len(), | ||
189 | )?); | ||
190 | } | ||
191 | Ok(()) | ||
192 | } | ||
193 | |||
194 | /// Returns matches for all added rules. | ||
195 | pub fn matches(&self) -> SsrMatches { | ||
196 | let mut matches = Vec::new(); | ||
197 | let mut usage_cache = search::UsageCache::default(); | ||
198 | for rule in &self.rules { | ||
199 | self.find_matches_for_rule(rule, &mut usage_cache, &mut matches); | ||
200 | } | ||
201 | nester::nest_and_remove_collisions(matches, &self.sema) | ||
202 | } | ||
203 | |||
204 | /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match | ||
205 | /// them, while recording reasons why they don't match. This API is useful for command | ||
206 | /// line-based debugging where providing a range is difficult. | ||
207 | pub fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> { | ||
208 | use base_db::SourceDatabaseExt; | ||
209 | let file = self.sema.parse(file_id); | ||
210 | let mut res = Vec::new(); | ||
211 | let file_text = self.sema.db.file_text(file_id); | ||
212 | let mut remaining_text = file_text.as_str(); | ||
213 | let mut base = 0; | ||
214 | let len = snippet.len() as u32; | ||
215 | while let Some(offset) = remaining_text.find(snippet) { | ||
216 | let start = base + offset as u32; | ||
217 | let end = start + len; | ||
218 | self.output_debug_for_nodes_at_range( | ||
219 | file.syntax(), | ||
220 | FileRange { file_id, range: TextRange::new(start.into(), end.into()) }, | ||
221 | &None, | ||
222 | &mut res, | ||
223 | ); | ||
224 | remaining_text = &remaining_text[offset + snippet.len()..]; | ||
225 | base = end; | ||
226 | } | ||
227 | res | ||
228 | } | ||
229 | |||
230 | fn output_debug_for_nodes_at_range( | ||
231 | &self, | ||
232 | node: &SyntaxNode, | ||
233 | range: FileRange, | ||
234 | restrict_range: &Option<FileRange>, | ||
235 | out: &mut Vec<MatchDebugInfo>, | ||
236 | ) { | ||
237 | for node in node.children() { | ||
238 | let node_range = self.sema.original_range(&node); | ||
239 | if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range) | ||
240 | { | ||
241 | continue; | ||
242 | } | ||
243 | if node_range.range == range.range { | ||
244 | for rule in &self.rules { | ||
245 | // For now we ignore rules that have a different kind than our node, otherwise | ||
246 | // we get lots of noise. If at some point we add support for restricting rules | ||
247 | // to a particular kind of thing (e.g. only match type references), then we can | ||
248 | // relax this. We special-case expressions, since function calls can match | ||
249 | // method calls. | ||
250 | if rule.pattern.node.kind() != node.kind() | ||
251 | && !(ast::Expr::can_cast(rule.pattern.node.kind()) | ||
252 | && ast::Expr::can_cast(node.kind())) | ||
253 | { | ||
254 | continue; | ||
255 | } | ||
256 | out.push(MatchDebugInfo { | ||
257 | matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) | ||
258 | .map_err(|e| MatchFailureReason { | ||
259 | reason: e.reason.unwrap_or_else(|| { | ||
260 | "Match failed, but no reason was given".to_owned() | ||
261 | }), | ||
262 | }), | ||
263 | pattern: rule.pattern.node.clone(), | ||
264 | node: node.clone(), | ||
265 | }); | ||
266 | } | ||
267 | } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) { | ||
268 | if let Some(expanded) = self.sema.expand(¯o_call) { | ||
269 | if let Some(tt) = macro_call.token_tree() { | ||
270 | self.output_debug_for_nodes_at_range( | ||
271 | &expanded, | ||
272 | range, | ||
273 | &Some(self.sema.original_range(tt.syntax())), | ||
274 | out, | ||
275 | ); | ||
276 | } | ||
277 | } | ||
278 | } | ||
279 | self.output_debug_for_nodes_at_range(&node, range, restrict_range, out); | ||
280 | } | ||
281 | } | ||
282 | } | ||
283 | |||
284 | pub struct MatchDebugInfo { | ||
285 | node: SyntaxNode, | ||
286 | /// Our search pattern parsed as an expression or item, etc | ||
287 | pattern: SyntaxNode, | ||
288 | matched: Result<Match, MatchFailureReason>, | ||
289 | } | ||
290 | |||
291 | impl std::fmt::Debug for MatchDebugInfo { | ||
292 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
293 | match &self.matched { | ||
294 | Ok(_) => writeln!(f, "Node matched")?, | ||
295 | Err(reason) => writeln!(f, "Node failed to match because: {}", reason.reason)?, | ||
296 | } | ||
297 | writeln!( | ||
298 | f, | ||
299 | "============ AST ===========\n\ | ||
300 | {:#?}", | ||
301 | self.node | ||
302 | )?; | ||
303 | writeln!(f, "========= PATTERN ==========")?; | ||
304 | writeln!(f, "{:#?}", self.pattern)?; | ||
305 | writeln!(f, "============================")?; | ||
306 | Ok(()) | ||
307 | } | ||
308 | } | ||
309 | |||
310 | impl SsrMatches { | ||
311 | /// Returns `self` with any nested matches removed and made into top-level matches. | ||
312 | pub fn flattened(self) -> SsrMatches { | ||
313 | let mut out = SsrMatches::default(); | ||
314 | self.flatten_into(&mut out); | ||
315 | out | ||
316 | } | ||
317 | |||
318 | fn flatten_into(self, out: &mut SsrMatches) { | ||
319 | for mut m in self.matches { | ||
320 | for p in m.placeholder_values.values_mut() { | ||
321 | std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out); | ||
322 | } | ||
323 | out.matches.push(m); | ||
324 | } | ||
325 | } | ||
326 | } | ||
327 | |||
328 | impl Match { | ||
329 | pub fn matched_text(&self) -> String { | ||
330 | self.matched_node.text().to_string() | ||
331 | } | ||
332 | } | ||
333 | |||
334 | impl std::error::Error for SsrError {} | ||
335 | |||
336 | #[cfg(test)] | ||
337 | impl MatchDebugInfo { | ||
338 | pub(crate) fn match_failure_reason(&self) -> Option<&str> { | ||
339 | self.matched.as_ref().err().map(|r| r.reason.as_str()) | ||
340 | } | ||
341 | } | ||