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